Hospitality Operations
An app that brings scheduling, inventory and six-supplier ordering together in one restaurant operation.
- since 2025
- live
- Architect & Implementer
- React Native, Expo SDK 54, Supabase, TypeScript, Playwright
The business problem
A restaurant ran scheduling, ordering and its public menu by hand. Weekly staff planning took 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.
There was also a blind spot. Nobody could see how a service actually paced: how long a table waited for drinks, and how long for food. Without that there was nothing concrete to improve against.
The people in the loop were 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 those suppliers offered a usable API. The job was to put all of it into one system that everyone could trust at the same time.
The solution
One app for owners, staff and the floor, with a single shared database underneath. Scheduling, inventory, ordering across six suppliers and the public website all read and write the same records, so a menu change or a reservation lands everywhere at once. NFC tags on the tables timestamp each step of service.
- ~2 h of WhatsApp back-and-forth
- ~10 min in the app
- handwritten notes, sometimes lost
- one live overview for the whole team
- the owner's job; orders got forgotten
- kept by staff in-app, orders generated
- invisible during service
- time-to-drinks and time-to-food, timestamped per table
- no channel on paper
- a feedback loop via reservation contacts
- asking the kitchen mid-service
- in-app for every server
- ~€200 per menu, outsourced
- cents per run, in-app LLM
- days of cross-system edits
- one in-app action
~2h → ~10min
~€200 → cents
since Dec 2025
The outcome
In production since December 2025, with features still landing. The wins are kept deliberately distinct, each scoped to exactly what it changed rather than summed into one number.
Scheduling dropped from about two hours of WhatsApp coordination to roughly ten minutes a week in the app; that figure is scheduling alone. Inventory and order generation was formerly a couple of hours of the owner's week and is now handled by staff directly in the app. That is a delegation gain, not a measured number.
Seasonal menu changes, three a year, went from several days of manual work across 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 captured digitally, so bookings no longer get lost, and the guest contact details that come with them opened a feedback loop that did not exist on paper.
The NFC layer unlocks something the restaurant never had. Because every status tap is timestamped, service times are measurable for the first time: time to drinks, time to food, table turnover. 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.
The engineering behind it
The architecture
One cross-platform app: owner web, staff phones, and a separate floor view. Underneath sits a single managed Postgres with row-level security and realtime, plus a server-rendered public website reading the same database.
Data is cached in tiers by how often it actually changes: live floor data refreshes in seconds, menus in days. One adapter wraps browser and native storage. Because both web and app react to the same row insert, a reservation entered anywhere fires exactly one confirmation.
The supplier layer was the hardest part of the build. None of the six offered a usable API. One had a webshop whose ordering endpoints I reverse-engineered into a programmatic order flow. A second exposed a semi-structured JSON feed. The third had nothing but a catalogue, scraped with Playwright.
Over those three access methods sits a normalization pipeline that fuzzy-matches the same product across all six suppliers, however differently each one names and packages it. Only then does a price comparison compare like with like, and only then can an order route to whoever is cheapest that week. Getting heterogeneous, undocumented sources to behave like one clean catalogue was the bulk of the engineering. It is also what turns ordering from a chore into a decision.
| Supplier source | Integration method | What breaks it |
|---|---|---|
| Webshop, no API | reverse-engineered ordering endpoints | a site redesign |
| Structured-ish | semi-structured JSON feed | schema drift |
| Catalogue only | Playwright-scraped | a 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 supplier layer that places the orders.
Seasonal menu turnover, three menus a year, used to be days of manual cross-system editing. Now it is 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 by tapping an NFC tag at the table: drinks served, food served, table cleared. Each tap is a timestamped event written to the same realtime Postgres backbone, so table state is shared instantly across the web, staff and floor views.
The database is the integration bus: the authoritative record 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 and there is no second system to update. 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 these suppliers simply do not publish. 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 does not 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. During a busy service, staff will not navigate menus. A friction-free tap on a tag at the table is what actually gets used, and adoption rather than the data model is the real constraint. In exchange, 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 that lock. It never returned.
The first attempt was a ten-second timeout. That stopped the 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.
Same problem in your own operation?
What I do for companiesHiring for an applied-AI or solutions-architecture role?
The full track record