Skip to main content

A few months ago a restaurant client asked me to fix a problem most small restaurants have: paper menus get sticky, prices change on a whiteboard, and waitstaff spend half their shift relaying orders to the kitchen. They wanted QR-code menus, a way for customers to order from their own phones, and a live dashboard for the kitchen.

The result is now in production at farresidency.m2hio.in — serving real customers daily. This post walks through the architecture, the parts I got right, and a few things I'd do differently next time.

The constraints

The brief came with hard constraints, which is actually great:

The stack

I picked boring, well-understood tech because production debugging matters more than novelty:

How orders flow

The core flow is simple, which is why it works:

  1. Customer scans the QR code at table 7. The URL contains ?table=7.
  2. The React app fetches the menu from /api/menu, cached for 60 seconds at the edge.
  3. Customer adds items, hits Place Order. The app POSTs to /api/orders with the table ID.
  4. The server writes the order to MongoDB and broadcasts a JSON message over the admin WebSocket channel.
  5. The kitchen dashboard sees the order pop in within ~150ms, prints a ticket, and updates status as cooking progresses.
  6. Each status update broadcasts back to the customer's open WebSocket — the table page shows "Preparing → Ready → Served" without a refresh.

The WebSocket layer

The kitchen channel is a single WebSocket connection per admin device. The customer side is per-table — when a customer opens table 7's menu, they subscribe to orders:table:7. Server-side I keep a simple in-memory map.

No Redis pub/sub. No clustered Node.js. One process, one machine, in-memory state. Yes, that means a restart drops connections — the client reconnects with exponential backoff and the in-flight orders are still in MongoDB. For a small restaurant this is more than enough; the entire ordering volume fits in a single server's memory by orders of magnitude.

Things I'd do differently

Move the menu to ISR-style caching, not in-memory

I cached the menu in process memory for performance, then realized when the client edits a price it doesn't propagate until I restart the server. I fixed this by adding a 60-second TTL with a manual invalidation endpoint, but I'd just put it on Cloudflare KV from day one.

Idempotency keys on order creation

On flaky restaurant Wi-Fi, customers sometimes double-tap "Place Order" before the first request returns. I added an idempotency key on the client after the first incident — should have been there from the start.

Print formatting deserves its own service

The kitchen ticket printer needed ESC/POS commands, which I crammed into the admin React app. It works but feels ugly. A small Go binary running on a Raspberry Pi as a print server would have been cleaner.

Numbers from production

After three months in production:

Takeaway

The hardest part of small-business software isn't picking the stack. It's shipping something the staff will actually use without complaining for a week. WebSockets felt like overkill until I saw the kitchen running on it live — and now removing that responsiveness feels unimaginable.

If you want to see the system in action, it's live at farresidency.m2hio.in. If you're building something similar and want to talk it through, I'm at m2hgamerz.prince@gmail.com.