Case studyLive
Masa
QR menu, table ordering, a kitchen display, a waiter tablet and an owner admin for independent cafés and restaurants in Türkiye.
- Role
- Solo — product, design implementation, backend, infra
- When
- 2026
- Backend
- Full-stack
The demo launcher opens the guest, kitchen, waiter and owner screens in one click each. Owner login: [email protected] / bostan-demo; test card 4242 4242 4242 4242. The demo venue resets every night; the source is private.

Who it’s for
- Owner-run neighbourhood places with 6–40 tables — a breakfast café in Kuzguncuk, a lokanta, a meyhane — that don’t want a POS contract or an app their guests have to download.
- They need a menu that’s always current (sold-out items, allergens, four languages for tourists), orders that reach the kitchen without a notepad, a kitchen screen that doesn’t drop tickets when the Wi-Fi does — and a number at the end of the day.
- Guests who scan the QR code on the table and order together: nothing to install, no sign-up.
What I built
- Built it alone, end to end: a Hono API on Cloudflare Workers, D1 for the catalogue and history, and one SQLite-backed Durable Object per venue that owns the live floor — carts, orders, calls and bills.
- Four front ends in one React app, ported 1:1 from an approved design prototype: the guest flow (TR/EN/DE/RU, allergen filter, shared cart, live order status, bill split), the kitchen display, the waiter tablet and the owner admin — plus a design-system page.
- Exactly-once delivery: every change carries a client-generated action id, waits in an IndexedDB outbox until it is acknowledged, and is deduplicated by the Durable Object in the same transaction that applies it. A service worker keeps the kitchen screen running offline.
- The owner’s side: a menu builder with modifiers and translations, printable QR table tents and an end-of-day report with CSV — plus PBKDF2 auth with httpOnly sessions, rate limits, a fake PSP behind a PaymentProvider interface, a nightly cron and a chaos test that proves the invariant.
How it works
1Scan
The QR tent on table 7 opens the menu. The guest picks a language (TR/EN/DE/RU), types a name and can hide allergens.
2Build the cart together
Each phone adds items with modifiers and a note for the kitchen; every phone at the table sees the same cart live, including who added what.
3Send
One tap sends the whole table cart. If two people press send at once, exactly one order goes out; if the cart changed while you were reading, nothing is sent until you’ve seen it.
4Kitchen → waiter → table
The ticket lands on the kitchen screen within milliseconds; its timer turns amber at 8 minutes and red at 15. The cook bumps it, the guest’s tracker moves on and the waiter sees it’s ready to serve.
5Pay
Split equally, by item or by a custom amount, add a tip and pay with the test card — or ask for the bill and pay at the counter. The table is then closed and archived.
Key decisions
01
One Durable Object per venue owns the live floor
Instead of: A shared database with locks or transactions across requests.
Why: Carts, orders, calls and bills need a single writer. A Durable Object serialises every action for free — no locks, no distributed transactions — and its output gate means an ack is never sent before the write is durable. D1 holds the catalogue and the history; the hot path never reads it.
02
Exactly-once by action ids, not by careful clients
Instead of: Retry and hope the server notices duplicates.
Why: A phone on café Wi-Fi only gets at-least-once delivery. A client-side outbox plus a server-side dedupe log turns that into exactly-once effects. Changes commute where they can — quantities are deltas, kitchen ticks set rather than toggle — and an order names the cart revision the guest confirmed.
03
Hibernating WebSockets for the fan-out
Instead of: Polling, or sockets that keep the object awake.
Why: Phones, kitchen screens and waiter tablets hold sockets tagged by role and table. Idle sockets cost no Durable Object time and pings are answered by the runtime without waking it. Only /api/* invokes the Worker; the app itself is served as free static assets.
04
Port the design, don’t approximate it
Instead of: Rebuild the screens with a UI kit that looks close enough.
Why: The approved prototype is the spec: its CSS is ported verbatim and its screens rebuilt 1:1 as React components — the tokens, the kitchen ticket, the table states and the TR/EN/DE/RU copy. Screens it only implied use the same tokens and components.
Architecture
One Worker deploy serves the app, the API and the WebSockets; the venue’s Durable Object is the only writer of the live floor. Everything here is mine.
Numbers
- lost / duplicate orders in the chaos run
- 0 / 0
- connection drops in that run — 4,336 actions replayed
- 535
- order → kitchen screen, p50 (p95 87 ms, local)
- 30 ms
- a month — Cloudflare’s free plan
- €0
Tech stack
Backend
- Cloudflare Workers
- Durable Objects
- D1 (SQLite)
- Hono
- TypeScript
- WebSocket hibernation
- Cron Triggers
- Rate Limiting
- PBKDF2
Frontend
- React
- React Router
- Vite
- Service worker
- IndexedDB
- qrcode-generator
Quality
- Vitest (Workers pool)
- Chaos script
- wrangler dry-run
Gallery

The kitchen screen offline: actions queue up and sync on reconnect — nothing is lost. 
The guest flow: menu, live order status, bill split. 
The waiter tablet: floor plan, calls and bill requests. 
The owner’s end-of-day report. 
Printable QR table tents, one per table.
Want the longer version — the code, the pipeline, the trade-offs?
Email me