S
supero.docs
Documentation/Reference apps/Tavola's restaurant ordering system puts diner and kitchen on one row

Tavola's restaurant ordering system puts diner and kitchen on one row

Tavola is an open-source restaurant ordering system with a kitchen board, reservations and loyalty points. Source on GitHub, live at tavola.supero.live.

  • apps
  • hospitality
  • restaurants
  • ordering
  • workflows

Overview

Four restaurants and twenty-five dishes in an open-source restaurant ordering system, with one order_state field that the kitchen writes and the diner watches.
Received, preparing, ready, out for delivery. Those are the four columns of the kitchen board in Tavola. The progress tracker a diner sees under "My orders" walks the same states and ends at Completed. Both are drawn from one field on one Order row, and no other app among the 19 in the repo has a kitchen board at all. The source is the tavola folder on GitHub.

A restaurant ordering system in five schemas and not one `extends`

Several apps in the repo take their booking or payment states from a platform lifecycle. Tavola does not.
schemas.py declares five ordinary schemas, one per table, and writes its own state lists near the top:
python
ORDER_TYPES = ["Pickup", "Delivery", "Dine-in"]
ORDER_STATES = ["received", "preparing", "ready", "out_for_delivery", "completed", "cancelled"]
PAY_STATES = ["unpaid", "paid"]
RESERVATION_STATES = ["requested", "confirmed", "seated", "completed", "cancelled", "no_show"]
Restaurant and MenuItem are public, so the menu loads without an account. On 2 October 2026 the live app's public endpoint returned four restaurants and 25 menu items, the same counts as the seed data, and answered 403 for order. OrderLine declares "parent_type": "order", making each line a child of its order. The cart never reaches the server. It lives in localStorage, the browser's own storage, holds dishes from one restaurant at a time, and asks before emptying itself when you add a dish from a different kitchen. Only at checkout does ui/app.js create the Order and then one OrderLine per dish. Paying by card calls services.stripe.checkout and redirects to the URL it returns. Choosing "Pay at the restaurant" skips that step.

The one move on the board that is a workflow

Most moves on the board are a single field update: received to preparing, ready to completed. The step from preparing to ready is different, since the customer has to be told. Its button runs order_ready from setup.py, a two-step workflow declared with "on_error": "compensate". This is the first step:
python
            {"id": "advance", "type": "crud_operation", "operation": "update", "object_type": "tavola:order",
             "record_uuid": "{{input.order_uuid}}", "data": {"order_state": "ready"},
             "compensate": {"kind": "automatic", "type": "crud_operation", "operation": "update",
                            "object_type": "tavola:order", "record_uuid": "{{input.order_uuid}}",
                            "data": {"order_state": "{{input.prev_state}}"}}},
The second step sends the SMS. Should that send fail, the definition tells the engine to put the order back in whatever prev_state the board passed in, so that a failed run does not leave behind a row claiming a customer was told their food was ready. The board's own error handler then sets ready with a plain update and, on main, warns the operator that no text went out.
A kitchen should not be blocked on a text message.
Two more workflows fire with no button involved. order_confirmation is bound to the @create:tavola:order event. reservation_confirmation is bound to @create:tavola:reservation. Each sends a confirmation email, and in both bindings the recipient is mapped from user.email, the address of the signed-in user who created the record. The email typed into the checkout form is not used for delivery.

Loyalty points with no points table

There is no ledger.
At checkout placeOrder writes points_earned on the order, the subtotal rounded to a whole number. The portal's balance is a reduce over the diner's orders. The diner's rule on order in setup.py carries filter_field="owner_username" with filter_match="$user.name", policy-based row scoping, so the list that comes back already belongs to that diner and the sum needs no filter of its own.
Tavola is the only app in the repo whose schema file mentions loyalty. The whole feature is one column and one reduce.

Four locations inside one tenant

A comment in config.py explains the choice: the restaurant group is a single tenant and its four locations are rows inside it, since the point of the app is that a diner who likes the trattoria can find the sushi counter and the cafe from the same account. Orders and reservations carry a restaurant_name. The operations console has a location switcher, and picking a restaurant there narrows the dashboard, the kitchen board and the reservations book to it. The policies name two roles. tenant_admin is the operations login, with menu editing on top of the board. tenant_user is the diner, who can read every menu but sees no order or reservation that is not their own. A reservation starts at requested when a diner books. The console moves it to confirmed, then seated, then completed.

Cook from the repo

The login screen at tavola.supero.live lists two demo accounts: a diner, and an operations user. Place an order as the diner, sign in as operations in another browser, and move the ticket across the board. Reload the diner's page and the tracker has caught up.
bash
git clone https://github.com/supero-platform/supero-apps
cd supero-apps/apps/hospitality/tavola
cp .env.example .env    # set SUPERO_DOMAIN and SUPERO_PASSWORD
./run.sh
Registration is open, so there is no account to create. A first run takes about two minutes. The seed gives the demo diner five orders with fourteen lines between them and five reservations spread over four states. The contents of the tavola folder are MIT. The platform is a hosted service, and its source is not published.
Change the 25 dishes in MENU first. When you want a schema that is yours from line one, start at https://www.supero.dev.