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:
- No customer app install. Whatever loads after a QR scan has to be a web page that's instantly usable.
- Real-time order updates. When the kitchen marks an order ready, the customer's phone should reflect it within a second.
- Single admin dashboard. Menu management, order monitoring, pricing — all in one place.
- Cheap to run. A small restaurant can't justify a $200/month infra bill.
The stack
I picked boring, well-understood tech because production debugging matters more than novelty:
- Node.js + Express for the API server. Familiar, fast enough, easy to deploy.
- MongoDB Atlas for menus and orders. Schema flexibility helps when the client wants a new field added live.
- WebSockets (ws) for the order-status bus. Pub/sub by table ID.
- React for the customer ordering app and the admin dashboard, sharing a small component library.
- Cloudflare for DNS, TLS, and caching the static menu images.
How orders flow
The core flow is simple, which is why it works:
- Customer scans the QR code at table 7. The URL contains
?table=7. - The React app fetches the menu from
/api/menu, cached for 60 seconds at the edge. - Customer adds items, hits Place Order. The app POSTs to
/api/orderswith the table ID. - The server writes the order to MongoDB and broadcasts a JSON message over the admin WebSocket channel.
- The kitchen dashboard sees the order pop in within ~150ms, prints a ticket, and updates status as cooking progresses.
- 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:
- Average order latency (place to kitchen seeing it): under 200ms end-to-end.
- WebSocket connection success rate: ~99.4%.
- Average orders per day: 80–150 depending on weekday.
- Total infra cost: under $15/month (Atlas free tier + a $5 VPS + Cloudflare free).
- Zero data-loss incidents. Two restart-related disconnect events, both auto-recovered.
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.