Inspiration
Every restaurant already has the information it needs — it's just scattered. A ticket on a spike, a table number in someone's head, a stock count nobody did. By the time you notice table nine has been waiting twenty-five minutes, they've noticed first. We wanted one screen where the room you actually drew is the room you actually run.
What it does
Serva is a hospitality operations platform for cafes, restaurants and bars.
Owners get a menu and inventory system where recipes link dishes to ingredients, so selling a plate deducts the stock behind it and shows live cost and margin. They draw their floor plan from the top down — tables, kitchen, bar, terrace — and the moment they save, that same plan becomes the live operations view, colour-coded, with numbered order pins and a kitchen display.
Customers scan one QR code per venue, claim a table by name and phone, browse, and order. No app to install. They track their order live with an honest ETA.
"Ask the menu" is our AI layer: a guest types anything — "what drinks do you have?", "something light and vegetarian", "what's the cheapest thing?" — and gets a real answer plus tappable dishes from that venue's live menu, each with a one-line reason and an Add button.
How we built it
Next.js 16 (App Router) · TypeScript · Tailwind v4 · shadcn/ui · Supabase (Postgres, Auth, Realtime) · TanStack Query · Cloudinary · Gemini
Two of us built it in parallel with a strict file-ownership split — one on the owner dashboard, one on the customer app — so we never edited the same file twice.
Two decisions we'd defend anywhere:
Wait times are computed once, in the database, when the order is placed — slowest item's prep time, plus 1.5 min per extra item, scaled by kitchen load and capped at 1.6×. Stored on the order so the countdown never jumps. The client only computes elapsed time and the green→amber→red colour.
Live updates poll every 2 seconds as the baseline, with Realtime layered on top purely to make it feel instant. Realtime alone fails silently on hosts that throttle websockets. With polling underneath, the worst case is a 2-second refresh instead of a dead screen.
Challenges we ran into
Making an LLM safe enough to touch a cart. A hallucinated dish reaching the kitchen is a real-world failure, not a bad demo. We guard it twice: any item id the model invents is dropped server-side against the real catalogue before it's returned, and picks are re-checked against current availability and stock before rendering. The model also never sees a price it could misquote back — it gets prices only to rank "cheapest", and the UI renders the real number.
Knowing when to refuse. Our menu has descriptions, not recipes. So when a guest asks "does this contain nuts?", the assistant says it can't confirm and to ask staff. Guessing there is a liability, not a feature.
Every model id we'd memorised was wrong. gemini-2.5-flash had been retired for new API keys — we only found the working model by querying Google's live model list. Then the endpoint returned bare 503s on 2 of 5 calls, so we added retry with backoff. Then we hit the free tier's real ceiling: 20 requests per day, per model — and discovered our own retry logic was burning three of them on a single question. Removing 429 from the retry list was the fix.
Next.js 16's sharp edges — the middleware.ts → proxy.ts rename, async request APIs, and four separate ways to make a Supabase query silently resolve to never.
Accomplishments that we're proud of
The whole loop works, end to end, in about a second: a customer taps Add on an AI suggestion, and a numbered pin appears on the owner's floor plan, a ticket lands on the kitchen display, and the countdown starts.
The AI feature adds zero new npm dependencies, zero schema changes, and degrades to invisible — no key, no network, no problem, the menu and ordering are untouched.
And when we merged two days of parallel work across two people, there were no conflicts at all. The ownership split held.
What we learned
Ship the failure path first. Every interesting bug this weekend was in what happens when something doesn't work — the retired model, the 503, the quota, the sold-out dish between the answer and the tap. The happy path took an afternoon; making it honest took the rest.
Also: never trust a model id from memory. Query the list.
What's next for Serva
- Real allergen screening, backed by the recipe and ingredient data the owner side already stores — the version of that question we're currently right to refuse
- Natural-language whole-order parsing — "two flat whites and a croissant"
- Menu translation with a caching layer, so a tourist reads the menu in their own language
- Staff accounts and roles, shift reports, and supplier reordering straight from low-stock alerts
Built With
- cloudinary
- next.js
- supabase
Log in or sign up for Devpost to join the conversation.