# Dabil — Architecture & Scaling Roadmap

_Last updated: July 2026_

Dabil is a multi-tenant restaurant ordering platform: customers scan a QR code
(table or office location), order from a restaurant's menu, and pay from a
wallet; restaurant admins, kitchen staff, and waiters manage the flow; a
super-admin panel oversees the platform. This document records how the system
is built today, what has been hardened for scale, and the ordered roadmap to
millions of users — designed so each step is incremental, with **no big-bang
rewrite required**.

---

## 1. Current architecture

```
┌────────────┐  ┌──────────────────┐  ┌───────────────┐  ┌─────────────┐
│ customer/  │  │ restaurantadmin/ │  │    staff/     │  │  / (root)   │
│  PWA-style │  │  admin panel     │  │ kitchen+waiter│  │ super-admin │
└─────┬──────┘  └────────┬─────────┘  └───────┬───────┘  └──────┬──────┘
      └──────────────────┴────────┬───────────┴────────────────┘
                                  │  PHP 8 (shared session auth per portal)
                          ┌───────┴────────┐
                          │  MySQL 8       │  single primary
                          └────────────────┘
```

- **Modularity**: four portals with separate session guards
  (`customer/includes/header.php`, `restaurantadmin/includes/header.php`,
  `staff/includes/auth.php`, root `header.php` for super-admin). AJAX
  endpoints live under each module's `ajax/`.
- **Payments**: wallet ledger (`wallets`, `wallet_transactions`) with unique
  transaction references; order placement is a single DB transaction covering
  order insert, items, inventory decrement, wallet debits, restaurant credit,
  and platform commission.
- **Schema management**: idempotent "self-heal" migrations
  (`staff/includes/schema_heal.php`, `restaurantadmin/includes/schema_indexes.php`,
  column checks in the headers) guarded by `information_schema`, cached per
  session. SQL mirrors live in `restaurantadmin/schema_*.sql`.

## 2. Multi-tenant isolation (enforced today)

**Rule: every read and write against tenant data carries `restaurant_id`
(staff/admin) or `user_id` (customer) derived from the server-side session —
never from client input.**

- Restaurant context (`$__rid` / `$__rest_id`) is loaded from the signed-in
  user's own row on every request. Client-supplied `rid` parameters are
  ignored for authorization (e.g. `restaurantadmin/ajax/receipt.php` derives
  the restaurant from the session — fixed from a former IDOR).
- Staff order actions verify ownership with a scoped SELECT **and** re-scope
  the UPDATE (`kitchen.php`, `waiter.php`) — defense in depth.
- Customers can only read their own orders/wallet/notifications
  (`WHERE user_id = ?` from session).
- Role separation inside a restaurant: destructive actions (delete menu
  items, tables, categories; edit settings) require `restaurant_admin`;
  staff/waiter roles get operational actions only (toggle availability,
  order status).
- Nothing assumes a single restaurant exists — all queries are scoped, all
  QR codes carry the restaurant id, and per-restaurant wallets are keyed by
  `restaurant_id`.

**Invariant for new code**: any new query touching `orders`, `menu_items`,
`categories`, `restaurant_tables`, `notifications`, `wallets`,
`wallet_transactions`, `qr_scans`, `service_calls`, or feedback tables MUST
filter by the session-derived tenant id. Code review should reject anything
else.

## 3. Database: relationships & indexing

Core relationships (all keyed by integer ids):

```
restaurants 1—N users(role=restaurant_admin|staff|waiter)
restaurants 1—N menu_items N—1 categories
restaurants 1—N restaurant_tables (area_type: table|location)
restaurants 1—N orders 1—N order_items (item name/price denormalized at
                                         purchase time — receipts stay correct
                                         even if the menu changes later)
users       1—N orders, wallets, wallet_transactions, customer_notifications
orders      1—1 table_label snapshot ("Table 5" / "ICT Office")
```

Hot-path composite indexes are self-healed on admin login
(`restaurantadmin/includes/schema_indexes.php`) and documented in
`restaurantadmin/schema_scale.sql`:
`orders(restaurant_id, order_status, created_at)`, `orders(user_id, created_at)`,
`order_items(order_id)`, `menu_items(restaurant_id, is_available, category_id)`,
`notifications(restaurant_id, is_read, created_at)`,
`wallet_transactions(user_id|restaurant_id, created_at)`, and more. Every
tenant-scoped query is index-backed; no full scans on growing tables.

Write-path notes:
- Inventory decrement is atomic (`UPDATE … WHERE stock_qty >= ?`) — safe under
  concurrent checkouts, no oversell.
- Wallet mutations are wrapped in one transaction with rollback on any step.
- Unbounded lists are paginated or capped (admin order history: 50/page;
  customer history: newest 100; live orders view: 200).

## 4. Security posture

- **SQLi**: prepared statements everywhere (parameterized `?` bindings).
- **XSS**: output escaped with `htmlspecialchars` at render time.
- **IDOR**: session-derived tenant/user scoping (section 2).
- **Uploads**: extension whitelist + size cap + `getimagesize`/`finfo`
  content validation (menu images, logos, avatars); server-generated random
  filenames; `uploads/.htaccess` denies execution of any script file type —
  uploaded files are data, never code.
- **Headers**: `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options:
  nosniff`, `Referrer-Policy: strict-origin-when-cross-origin` on all portals.
- **Sessions**: role-gated portals; forced first-login password reset for
  staff; destructive actions restricted to admins.
- **Redirect safety**: output buffering in headers so PRG redirects always
  work (no half-rendered pages after state changes).

### Known gaps (next hardening passes — do these before public scale)
1. **CSRF tokens** on state-changing POST forms (a `csrf.php` helper exists at
   the root; wire it into admin/staff forms first, then customer forms).
   Retrofit form-by-form with tests — do not bulk-replace.
2. **Rate limiting** on login, checkout, and polling endpoints (simple
   per-IP/per-user counters in the DB now; move to Redis later).
3. **Session cookie flags** (`HttpOnly`, `Secure`, `SameSite=Lax`) set via
   `session_set_cookie_params()` before each `session_start()`, or in
   `php.ini`/`.user.ini` — do it platform-wide in one change window.
4. **Password/2FA review** on the auth flows (webauthn + 2FA already exist on
   the customer side; extend to admin portal).

## 5. Performance & caching

Today:
- Composite indexes (section 3) + bounded queries keep p95 latency flat as
  data grows.
- Polling endpoints (`ajax/poll.php`, service calls) are single-row indexed
  lookups; 20-second intervals.
- QR codes render client-side (no server image generation).

Next (in order, each independent):
1. **HTTP caching for static assets**: far-future `Cache-Control` +
   versioned filenames for CSS/JS/images via `.htaccess`.
2. **Application cache** for menu reads (the hottest read path — every
   customer scan): start with per-request memoization, then APCu/Redis with
   a `menu:{rid}:{version}` key invalidated on menu writes.
3. **Replace polling with push**: web push already exists; move order-status
   updates to server-sent events or a lightweight websocket service when
   concurrent users make polling expensive.

## 6. Scaling roadmap (no-rewrite path)

Each stage is additive; the code changes are localized because tenant scoping,
bounded queries, and modular portals are already in place.

**Stage 1 — now → ~50k users (current shared/VPS hosting)**
- Done: indexes, pagination, isolation, upload hardening, security headers.
- Do: CSRF + rate limiting + cookie flags (section 4), nightly DB backups
  with restore drills, uptime monitoring, error-log alerting.

**Stage 2 — dedicated infra (~50k → 500k users)**
- Move MySQL to a managed instance; add one **read replica**; route heavy
  read pages (history, analytics, super-admin reports) to it.
- Add **Redis**: sessions (makes PHP nodes stateless), menu cache, rate
  limiting. Session handler change is one config, not a rewrite.
- Move `uploads/` to **object storage (S3-compatible) + CDN**; code already
  funnels uploads through 3 small handlers, so the change is swapping
  `move_uploaded_file` for an SDK `putObject` and storing URLs. Serve all
  static assets from the CDN.
- Run 2+ PHP app nodes behind a load balancer (stateless once sessions are
  in Redis). Zero code changes required beyond the session handler.

**Stage 3 — regional scale (500k → millions, multiple countries)**
- **Queue** (e.g. SQS/RabbitMQ/Redis streams) for non-critical writes:
  notifications, web push, receipts, analytics events — checkout transaction
  shrinks to order+wallet only.
- **Partitioning**: `orders` and `wallet_transactions` partitioned by month
  (they're append-mostly); archive closed orders older than N months to a
  warehouse for analytics.
- **Multi-region**: one primary region per country/currency (data residency +
  latency), CDN everywhere, GeoDNS routing. The tenant model already keys
  everything by `restaurant_id`, so sharding tenants across regional
  databases is a routing-layer concern, not a schema rewrite.
- **Observability**: structured logs, APM (query timings), dashboards on
  order throughput, wallet integrity checks (ledger sum == balance) as a
  scheduled job.

## 7. Rules for future development (avoid re-introducing debt)

1. Scope every tenant query by session-derived `restaurant_id`/`user_id`.
2. Never trust client-supplied ids for authorization — only for selection
   within the tenant's own data.
3. Every new list page ships with pagination (or a hard LIMIT) from day one.
4. Every new column/index ships as an idempotent self-heal + a line in the
   matching `schema_*.sql`.
5. New uploads go through content validation and (Stage 2+) object storage.
6. Money paths: single transaction, unique references, rollback on any
   failure, never trust client-side amounts.
7. Prefer adding a module folder (like `staff/`) with its own guard over
   growing an existing page — keeps portals independently deployable later.
