Hospitality Operations
A PWA driving scheduling, inventory, and supplier integrations for a mid-volume regional restaurant.
- 2025—
- live
- Architect & Implementer
- React Native, Expo SDK 54, Supabase, TypeScript, Playwright
A ~20-person restaurant ran scheduling, ordering, and its menu by hand. This PWA puts scheduling, inventory, six-supplier ordering, and a live website in one system.
- ~2 h of WhatsApp back-and-forth
- ~30 min in the app
- ~€200 per menu, outsourced
- cents per run, in-app LLM
- days of cross-system edits
- one in-app action
~2h → ~30min
~€200 → cents
since Dec 2025
The technical breakdown
The business problem
A mid-volume regional restaurant ran scheduling, ordering, and its public menu by hand. Weekly staff planning was about two hours of WhatsApp back-and-forth; supplier orders got forgotten; the public website lagged the real menu, so guests arrived on stale information; and handwritten reservations sometimes got lost. And no one could see how a service actually paced — how long a table waited for drinks, then food — so there was nothing concrete to improve against.
The people in the loop: around twenty staff and the owners who plan shifts and place orders, the guests who book and eat, and six B2B suppliers whose catalogues and prices change constantly — none of whom offered a usable API. The job was to put all of that in one system that everyone could trust at the same time.
The architecture
A cross-platform PWA — owner web, staff phones, a floor view — over a single managed Postgres with row-level security and realtime, plus a server-rendered public website sharing the same database.
Data is cached in tiers by how often it actually changes — live floor data refreshes in seconds, menus in days — behind one adapter over browser and native storage. The public site renders from the same database the kitchen edits, and both web and app react to the same row insert, so a reservation entered anywhere fires exactly one confirmation.
The supplier layer was the hardest part of the build. None of the six B2B suppliers offered a usable API. One had a webshop whose ordering endpoints I reverse-engineered into a programmatic order flow; another exposed a semi-structured JSON feed; a third had nothing but a catalogue, scraped with Playwright. Over those three completely different access methods sits a normalization pipeline that fuzzy-matches the same product across all six suppliers — so a price comparison compares like with like, and an order can route to whoever is cheapest that week. Getting heterogeneous, undocumented sources to behave like one clean catalogue was the bulk of the engineering, and it's what turns "ordering" from a chore into a decision.
| Supplier source | Integration method | Fragility |
|---|---|---|
| Webshop, no API | reverse-engineered ordering endpoints | breaks on a site redesign |
| Structured-ish | semi-structured JSON feed | breaks on schema drift |
| Catalogue only | Playwright-scraped | breaks on markup change |
One change, many destinations. This is where the integration bus pays off. A single menu edit propagates from one place: the public website updates instantly, the stored recipes that drive portion and quantity calculations update with it, and those quantities feed the reverse-engineered supplier layer that places the orders. Seasonal menu turnover — three menus a year — used to be days of manual cross-system editing; now it's one in-app action. Menu translation rides the same path: an in-app LLM renders each menu into the other languages, replacing a roughly €200-per-menu professional job with a cents-per-run call.
Live table status over NFC. Staff set a table's live state — drinks served, food served, table cleared — by tapping an NFC tag at the table. Each tap is a timestamped event written to the same realtime Postgres backbone (another row insert → notify producer), so table state is shared instantly across the web, staff, and floor views.
The database is the integration bus: the source of truth and the event are the same row.
Decisions & trade-offs
Cache TTL per data type, not one global constant. Sizing freshness by volatility killed both the staleness bugs and the needless round-trips a single TTL caused. The cost is one invariant you have to hold everywhere: every write invalidates its cache key.
One website, always in sync with the menu. The public site reads from the same database the kitchen edits, so a menu change is live immediately — no second system to update, no lag for guests to trip over. The trade-off is that the public site inherits the operational database's shape rather than a tidy content model of its own.
Reverse-engineer the suppliers, and own the fragility. Building against undocumented webshop endpoints and a scraper was the only way to get prices the suppliers simply don't publish — but those integrations break whenever a supplier redesigns their site. The trade-off was explicit: a brittle integration that delivers live price comparison beats a stable one that doesn't exist. Each source is isolated and validated independently, so when one breaks it degrades alone instead of taking the catalogue down with it.
NFC tap over in-app navigation. Live status is set by a physical tap on a tag at the table, not by digging through app screens. During a busy service, staff won't navigate menus — a friction-free tap is what actually gets used, and adoption, not the data model, is the real constraint. The trade-off: NFC tags are physical infrastructure to place and maintain, and the data is only ever as good as staff remembering to tap.
What broke
The nastiest bug was a web-only infinite spinner on resume. The browser serializes auth-token refresh through a lock, and on resume a data read waited on a lock that never returned. The first fix — a 10-second timeout — made the spinner settle instead of hang, but the next read failed identically, because the token was still dead. The real fix refreshed the session before the post-resume read storm.
A timeout buys you a survivable UX; only asking "what fails on the next attempt?" gets you the cause.
The outcome
In production since December 2025, with features still landing — and the wins are kept deliberately distinct, each scoped to exactly what it changed rather than summed into one number.
Scheduling dropped from ~2 hours of WhatsApp coordination to ~30 minutes a week in the app (that figure is scheduling alone). Inventory and order generation, formerly a couple of hours of the owner's week, is now handled by staff directly in the app — a delegation and capability gain, not a measured number. Seasonal menu changes (three menus a year) went from several days of manual cross-system work — website, stored recipe quantities, and the supplier links they feed — to a single in-app action. Menu translation moved from a professional job at roughly €200 per menu to an in-app LLM translation costing cents. Reservations are now captured digitally, so bookings no longer get lost, and the guest contact details that come with them opened a feedback loop that didn't exist on paper.
The NFC layer unlocks something the restaurant never had: because every status tap is timestamped, service times — time-to-drinks, time-to-food, table turnover — become measurable for the first time. No numbers are claimed yet; the capability itself is the result.
Planned, not shipped: the live "table free" signal will feed real-time reservation capacity on the public website, so online booking reflects actual floor availability. The data already flows on the bus — wiring it into the public booking flow is the next step.