Tableside AI restaurant ordering assistant

Making restaurant AI answer to the menu.

A diner wants useful menu guidance. A language model left unchecked can invent dishes, prices or allergens, or claim cart changes that never happened.

An AI restaurant ordering assistant. Diners scan a table's QR code, browse the menu and can order through a conversational waiter. Owners manage menus, tables and staff, and the kitchen works from a live order board. The assistant acts only through narrow, validated tools that call the same server-side cart and order functions as the regular interface. Prices, availability and order confirmation are enforced below the model, not in the prompt.

Role
Sole builder: product design, database and security model, AI tool layer, interface and deployment. Developed with AI coding assistance.
Type
Deployed product
Status
Deployed web app · private repository
Stack
  • Next.js (App Router)
  • React
  • TypeScript
  • Supabase (PostgreSQL, Auth, Realtime, row-level security)
  • Stripe Connect
  • Zod
  • Tailwind CSS
  • Vitest
  • Playwright
  • GitHub Actions
  • Vercel
  • Anthropic or OpenRouter models via a provider abstraction
Mobile menu for Demo Kitchen (Synthetic), described as synthetic demo data. A prompt card invites the diner to ask the waiter, above search, dietary filters and category tabs.
Menu, with dietary filters and the waiter entry point.
Dish page for Chili Garlic Shrimp Noodles, $17.00, marked hot, about 14 minutes. An allergen box reads "Contains Gluten, Shellfish, as listed by the restaurant", with advice to check with staff.
Dish detail. Allergens appear only once the restaurant has confirmed them.
Waiter chat. Asked for vegetarian dishes that aren't spicy, it lists soup, flatbread, salad, grain bowl, wrap and iced tea with menu prices, grouped by the restaurant's categories.
A live reply. It leaves out the spicy curry, the non-vegetarian dishes and an item marked unavailable.
Waiter chat. Asked to add a tomato basil soup and place the order, it shows an order summary (subtotal $7.00, tax $0.46, total $7.46) and asks "Shall I go ahead and place this order?"
"Add it and place the order" gets a server-priced summary and a question, not a submitted order.

Product screenshotsSynthetic demo restaurant, not a real business. Captured 2026-09-23 from a local build.

At a glance

Problem
A fluent assistant that is wrong about a price, an allergen or what is in the cart does more harm than no assistant.
Built
QR-to-order diner flow, conversational waiter, owner dashboard, live kitchen board and card payments, all on one set of validated server functions.
Evidence
Screenshots of the real app on synthetic data, plus documented failures from live testing, each paired with the guard or test that now prevents it.

Scope. The source repository is private and no public demo is linked here. The screenshots use a synthetic demo restaurant created for this page, not a real business, menu or price list. Allergen and dietary details shown to diners are entered by the restaurant, and the app does not verify them.

The workflow problem

At a table, a diner wants the kind of help a good server gives. What is vegetarian? What is not too spicy? What comes with this dish? A language model can hold that conversation. It can also quote a price the menu does not list, describe an allergen the restaurant never recorded, or tell the diner that an item is in their order when it is not. In a restaurant, each of those errors reaches a real bill or a real plate.

What makes it hard

  • A fluent answer is not a correct one

    A model that skips the menu lookup and answers from memory responds faster and sounds just as sure. Latency and tone cannot be used to judge correctness.

  • The prompt is not a control

    Instructions to confirm before ordering are advice to the model, not enforcement. In live testing, a model treated "Place the order" as confirmation and submitted immediately.

  • Restaurant data is incomplete and local

    Preparation times go unrecorded and categories like "Chaat" mean nothing at a pizzeria. A missing field has to read as "not specified", never as zero or as a guess.

  • Diners are anonymous

    A QR scan creates an anonymous session without signup. It must never be treated as a signed-in restaurant operator.

My approach

I made the database the only source of truth for the menu, prices, availability, allergens and orders, and gave the model no direct access to it. The model can only call a fixed set of named tools. Each tool validates its arguments with a strict schema and calls the same server functions that the ordinary "Add to cart" and "Place order" buttons call. There is no separate AI ordering path to get wrong.

Restaurant, table, session and every price are derived on the server from the diner's session, never taken from the model. When live testing exposed a failure the prompt could not prevent, I moved the rule into code: order confirmation, cart-state claims, currency on every price.

System architecture

Each stage is labeled by what it is: interface, deterministic logic, stored data, generated data, or the language model. The list reads in the order data flows.

  1. 01 InterfaceDiner interfaceMobile menu with search, filters and item details, plus a chat panel. Ordering works fully without the assistant.
  2. 02 Language modelConversational waiterChooses which tool to call and phrases the reply. It has no database access, no credentials and no generic query tool.
  3. 03 Deterministic logicTool layerNamed tools with strict Zod schemas that reject unknown fields. Restaurant and session context is built on the server once per turn.
  4. 04 Deterministic logicCart and order functionsThe same server actions the buttons use. Prices are computed on the server, and order submission is transactional and idempotent.
  5. 05 Stored dataPostgreSQL with row-level securityMenu, prices, availability, allergens and orders, isolated per restaurant and per diner session.
  6. 06 InterfaceKitchen board and owner dashboardLive order board over Realtime with a polling fallback. Menu, table, QR and staff management for owners.

If the model is unconfigured or its provider is down, the assistant is simply not offered. The menu, cart, checkout and kitchen board keep working.

Walkthrough

Example A captured session on synthetic data: a local build against a disposable test database, restaurant "Demo Kitchen (Synthetic)", 2026-09-23. Tool calls are taken from the app's own conversation log. The model's wording varies between runs.

  1. Input

    The diner asks: “Which vegetarian dishes do you have that aren't spicy?”

  2. System processing

    The waiter calls get_restaurant_info, then search_menu. Both are scoped on the server to this restaurant from the diner's session; the model never supplies a restaurant id.

    • Each returned item carries the restaurant's recorded dietary flags, spice level, availability and price, with the currency code.
  3. Output

    Six dishes with their menu prices, grouped by the restaurant's own categories. The one spicy vegetarian dish is named and left off. The non-vegetarian dishes and an item marked unavailable are not mentioned.

  4. Next action

    “Please add one Tomato Basil Soup and place the order.” The log shows get_menu_item, add_item_to_cart, then request_order_confirmation. The reply is a server-priced summary ($7.00 + $0.46 tax = $7.46) and a question. No submit_order call was made and no order exists; submission is rejected on the server until the diner confirms this cart.

Key engineering decisions

  1. AI tools call the same functions as the interface.

    Why
    One validated path for cart and order changes means the assistant cannot overcharge, sell an unavailable item or skip a check the buttons enforce.
    Tradeoff
    Tool shapes are constrained by existing server actions; one needs a small adapter to reuse a form-based action.
    Where
    docs/ai.md
  2. The model never supplies restaurant, session or price.

    Why
    Those values come from the server-side session. No tool schema accepts a restaurant id, a unit price or a total, and tests check each schema for that.
    Tradeoff
    The model cannot handle a request that genuinely spans restaurants. For this product, that is intended.
    Where
    tests/unit/ai-tool-schemas.test.ts
  3. Order confirmation is enforced on the server.

    Why
    A live test showed a model treating "Place the order" as confirmation. Submission now requires a prior confirmation step, tied to a fingerprint of the cart that was shown.
    Tradeoff
    One extra conversational turn before every AI-placed order.
    Where
    src/lib/ai/tools/order-confirmation.ts
  4. Models are vetted by their failure mode, not by speed.

    Why
    A model that skips the tool call is faster and wrong. Candidate models are probed for whether they call the tool before claiming an action, and preference lists allow fallback when a provider stops serving.
    Tradeoff
    Free model tiers rate-limit and change without notice, so vetting has to be repeated.
    Where
    scripts/vet-models-by-job.ts

Reliability and evaluation

What this measures. Reliability work here comes from automated tests and from failures observed in live testing, not from a scored benchmark. No accuracy percentage is claimed.

Test conditions

  • Unit, database/row-level-security integration and Playwright end-to-end tests, run by GitHub Actions on every push.
  • Static tests that the tool surface exposes no generic query or admin tool and accepts no price fields.
  • Live conversations against real model providers during development.
  • For this page: a local build with a synthetic restaurant, where the waiter was asked to add a dish and place the order in one message. It returned a server-priced summary and asked for confirmation instead of submitting.

Observed findings

  • A router model told a diner that an item had been added to their order without ever calling the add-to-cart tool. A cart-state guard now blocks claims about cart contents unless the cart was actually read that turn.
  • Asked about an Indian menu priced in US dollars, a model quoted ₹9.99 for a $9.99 dish. Every price now carries the restaurant's currency code, and the prompt forbids inferring one.
  • The item-detail tool advertised ingredients its query never selected, so the waiter said "the menu doesn't specify". A test now asserts that each tool returns the detail its description promises.
  • A queue estimate reported 12 orders and 24 minutes ahead because deleted dishes counted as zero minutes. A missing preparation time now makes the estimate unknown instead of instant.

Source: docs/ai.md and the unit tests referenced in the repository (private)

Limitation. There is no fixed conversation benchmark yet, so guard coverage is shown by tests and documented incidents rather than a measured rate. The Anthropic provider path has not been exercised against a live diner conversation.

What I learned

The prompt is not the control. I started with careful instructions: confirm before ordering, never invent a menu fact. Live testing showed a model reading "Place the order" as confirmation, and another claiming an add it never made. Each time, the fix was to move the rule below the model into code the model cannot talk its way around, and to add a test that fails if the rule is removed.

Current boundary and next step

Status: Deployed web app · private repository.

Not yet validated or not built

  • Allergen and dietary information is only as accurate as what the restaurant enters.
  • No measured conversation benchmark yet.
  • The free model tier is capped per day and is not sized for real diner traffic.
  • The repository is private, so the code can't be publicly inspected.
  • The screenshots come from one session on synthetic data. Model replies vary between runs.

Most valuable next experiment

Build a fixed conversation benchmark covering menu grounding, confirmation, cart-state claims and currency, and run it against every candidate model before it serves diners. That would turn the documented incidents into a measured rate.

Source and navigation

  • Guard behavior, described in detailThe walkthrough and decisions below describe the tool layer, the confirmation check and the cart-state guard. The repository is private; code review is available on request.

Talk about this work

Questions about the design, the tradeoffs or the evaluation are welcome.