Three surfaces — a customer app, a kitchen view, and an admin dashboard — that all had to agree about one order.
That is the entire success criterion, defined before a single screen was designed. Every UX decision, every state machine, every architectural trade-off defers to this rule.
No app download, no login, no account. The table QR is the entry point. Scan → menu in under two seconds on 3G.
Customers are anonymous. A UUID device_token in sessionStorage scopes cart and orders per browser tab. Zero friction, zero forms.
Individual logins (email & password) created by the Owner. Role-based access: Owner, Manager, Cashier, Waiter, Kitchen, Bartender, each scoped to what they need.
Order integrity is the hard constraint. No duplicate orders, no lost orders, no terminal states you can't recover from. Every state has a single owner and a defined transition trigger.
CREATED ──► PREPARING ──► READY
│ │
│ (customer claims payment) │
└────────► PAYMENT_PENDING ◄───┘
│
┌─────────┴─────────┐
▼ ▼
CONFIRMED CANCELLED
(admin verified) (abandoned / force-close)NOT_PAID ──► CLAIMED ──► VERIFIED (admin manual verify)
│
└──► CONFIRMED (auto on order confirm)ACTIVE ──► CLOSED
(admin closes table, or all session orders are paid)OSHAP-{tableId}-{4-digit-random}; order ref · bank-transfer reconciliation ref · one and the sameTrue real-time via SSE + push: instant updates without the overhead of WebSockets.
Live screens (the waiter dashboard, the pay page, My Orders) subscribe to a server-sent events stream. State changes push instantly from the backend the moment they happen, with no client polling loop.
FCM push covers the instants that feel urgent: new order, waiter called, POS requested. When the admin tab is hidden the service worker shows OS notifications. When it's open the AlertCenter fires a chime and queues a toast.
Modern SPA build per app inside an npm workspaces monorepo: `apps/customer`, `apps/admin`, `apps/platform`, `packages/shared`.
CSS-first `@theme` block in `packages/shared/src/tokens/tokens.css`; single design token source. Semantic utilities auto-swap on OS dark-mode preference.
Server-sent events for live screens: instant push from backend, no polling. Typed `fetch` wrappers in `packages/shared`. Optimistic cart updates.
Admin web push, foreground + background. Seven trigger types. Web Audio two-tone chime; no asset file.
SPA routing per app. `vercel.json` SPA fallback handles deep links on Vercel static deploy.
Installable to home screen: `display: standalone`, manifest, maskable icon, iOS apple-touch-icon. No offline data cache (dashboard needs live state).
Data-layer test suite covering mock-API handlers, shared utilities, and TanStack Query integration. Tree-shaken from production builds.
Customer app (with notification center), admin PWA (six modules: dashboard, kitchen, history, menu + inventory, analytics, settings), platform portal, shared package, OpenAPI contract, DDL: all shipped. Backend team wires FastAPI against the spec.
Deploy to 1–2 restaurants. Monitor scan-to-order conversion, verification latency, kitchen throughput, and payment completion rate.
Paystack or Flutterwave card payments and a tip flow. No separate payment flow on the backend — same verify handler.
Loyalty system, customer profiles, repeat-order recommendations, pre-ordering, reservations, and optional native mobile wrappers.
The temptation in hospitality tech is to solve for the happy path: customer who knows what they want, places their order in thirty seconds, and pays immediately. That customer is maybe 20% of a restaurant. The other 80% are groups negotiating, people who order in rounds, customers who need to flag a waiter mid-meal, tables that share a bill and split it unevenly.
Order Together, Request-a-POS, and the service bell exist because I refused to design only for the happy path. The state machines exist because payment integrity is non-negotiable: a missing CONFIRMED state or a duplicate order means real money lost by a real business.
The hardest constraint was the zero-auth guest. Every session-management pattern I reached for assumed an authenticated user. Designing around a disposable UUID that lives in sessionStorage; making group ordering work on top of that changed how I think about identity at the boundary between anonymous public access and shared real-time state.