Case study · written version

Oshap

Three surfaces — a customer app, a kitchen view, and an admin dashboard — that all had to agree about one order.

Role
Product Designer & Engineer · Founder
Read time
~12 min
See it working
/work/oshap
The problem

Restaurants run on shouting, paper, and lost revenue.

Guest
  • —Waits to catch a waiter's eye
  • —Repeats the order aloud
  • —Has no idea where it is in the kitchen
  • —Chases the bill at the end, sometimes twice
Merchant
  • —Order errors from manual capture
  • —Payment leakages: unclaimed bills, disputed cash
  • —No live visibility into active tables
  • —Kitchen and front-of-house running on shouting
The core bet

Faster than calling a waiter.

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.

01 / DECISION

QR-first entry

No app download, no login, no account. The table QR is the entry point. Scan → menu in under two seconds on 3G.

02 / DECISION

Zero-auth guest

Customers are anonymous. A UUID device_token in sessionStorage scopes cart and orders per browser tab. Zero friction, zero forms.

03 / DECISION

One staff surface

Individual logins (email & password) created by the Owner. Role-based access: Owner, Manager, Cashier, Waiter, Kitchen, Bartender, each scoped to what they need.

Systems & lifecycles

Three state machines under the surface.

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.

Order lifecycle
CREATED ──► PREPARING ──► READY
   │                               │
   │   (customer claims payment)   │
   └────────► PAYMENT_PENDING ◄───┘
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
      CONFIRMED           CANCELLED
   (admin verified)    (abandoned / force-close)
Payment lifecycle
NOT_PAID ──► CLAIMED ──► VERIFIED   (admin manual verify)
                    │
                    └──► CONFIRMED  (auto on order confirm)
Session lifecycle
ACTIVE ──► CLOSED
           (admin closes table, or all session orders are paid)
Reference format
OSHAP-{tableId}-{4-digit-random}; order ref · bank-transfer reconciliation ref · one and the same
Real-time, honestly
True 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.

Stack

Three apps. One shared package. One token source.

Vite 6 + React 19 + TypeScript

Modern SPA build per app inside an npm workspaces monorepo: `apps/customer`, `apps/admin`, `apps/platform`, `packages/shared`.

Tailwind CSS v4

CSS-first `@theme` block in `packages/shared/src/tokens/tokens.css`; single design token source. Semantic utilities auto-swap on OS dark-mode preference.

TanStack Query v5 + SSE

Server-sent events for live screens: instant push from backend, no polling. Typed `fetch` wrappers in `packages/shared`. Optimistic cart updates.

Firebase Cloud Messaging

Admin web push, foreground + background. Seven trigger types. Web Audio two-tone chime; no asset file.

React Router v7

SPA routing per app. `vercel.json` SPA fallback handles deep links on Vercel static deploy.

Admin PWA

Installable to home screen: `display: standalone`, manifest, maskable icon, iOS apple-touch-icon. No offline data cache (dashboard needs live state).

Vitest + jsdom

Data-layer test suite covering mock-API handlers, shared utilities, and TanStack Query integration. Tree-shaken from production builds.

Roadmap

From a printed QR to a restaurant OS.

NOW

Extended MVP shipped

BACKEND HANDOFF

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.

NEXT

Pilot venues

ROADMAP

Deploy to 1–2 restaurants. Monitor scan-to-order conversion, verification latency, kitchen throughput, and payment completion rate.

THEN

Payment gateways

ROADMAP

Paystack or Flutterwave card payments and a tip flow. No separate payment flow on the backend — same verify handler.

LATER

CRM · Loyalty · Reservations · Native

ROADMAP

Loyalty system, customer profiles, repeat-order recommendations, pre-ordering, reservations, and optional native mobile wrappers.

Reflection

What designing for a venue taught me.

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.