Skip to content

Case study

Hospitality Operations

An app that brings scheduling, inventory and six-supplier ordering together in one restaurant operation.

Year
since 2025
Status
live
Role
Architect & Implementer
Stack
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.

At a glance: staff, floor, the public site, NFC taps, and six suppliers all feed one shared database that drives scheduling, ordering, and live operations.
At a glance

Before → after

Weekly staff scheduling
Before~2 h of WhatsApp back-and-forth
After~10 min in the app
Reservations
Beforehandwritten notes, sometimes lost
Afterone live overview for the whole team
Inventory & ordering
Beforethe owner's job; orders got forgotten
Afterkept by staff in-app, orders generated
Table pacing
Beforeinvisible during service
Aftertime-to-drinks and time-to-food, timestamped per table
Guest feedback
Beforeno channel on paper
Aftera feedback loop via reservation contacts
Dish information
Beforeasking the kitchen mid-service
Afterin-app for every server
Menu translation
Before~€200 per menu, outsourced
Aftercents per run, in-app LLM
Seasonal menu change
Beforedays of cross-system edits
Afterone in-app action

~2h → ~10min

weekly staff scheduling

~€200 → cents

menu translation

since Dec 2025

in production

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 itExpand · ~4 min

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.

Client surfaces, meaning the public website, the staff app, the floor view and the NFC table taps, all write to one shared Postgres with row-level security and realtime. A row insert triggers tiered cache updates and email and push confirmations.
Client surfaces, meaning the public website, the staff app, the floor view and the NFC table taps, all write to one shared Postgres with row-level security and realtime. A row insert triggers tiered cache updates and email and push confirmations.

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.

Six B2B suppliers reached three different ways: a reverse-engineered ordering API, a semi-structured JSON feed, and a Playwright-scraped catalogue. All three feed one fuzzy product-normalization layer that reconciles the same product across vendors before it reaches the shared database.
Six B2B suppliers reached three different ways: a reverse-engineered ordering API, a semi-structured JSON feed, and a Playwright-scraped catalogue. All three feed one fuzzy product-normalization layer that reconciles the same product across vendors before it reaches the shared database.
Supplier sourceIntegration methodWhat breaks it
Webshop, no APIreverse-engineered ordering endpointsa site redesign
Structured-ishsemi-structured JSON feedschema drift
Catalogue onlyPlaywright-scrapeda 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.

Sequence of the resume spinner bug: on resume the data read waits on the browser's auth-token-refresh lock. A ten-second timeout ends the spinner, but the next read fails because the token is still dead. The fix refreshes the session before the post-resume read storm.
Sequence of the resume spinner bug: on resume the data read waits on the browser's auth-token-refresh lock. A ten-second timeout ends the spinner, but the next read fails because the token is still dead. The fix refreshes 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 companies

Hiring for an applied-AI or solutions-architecture role?

The full track record