← Back to projects
Project · 01 / 15NOV 1, 2026DINE-IN ORDERING · WEBLive now

Vivere Bay Cafe

A dine-in ordering system for one cafe. Guests scan the code on their table, order in rounds from their own phone and pay once when they leave. Staff run a live orders board, and the owner manages the menu, tables, staff and memberships.

Building Vivere

Vivere is the ordering system for Vivere Bay Cafe. You scan the code on your table, order from your phone, keep ordering as the evening goes on, and pay once when you leave. It's client work, built for one cafe in India, and it's live at vivere-bay.vercel.app/cafe.

The owner came with a clear picture of how service should feel: nobody waits for a menu, nobody pays at the counter before they sit down, and the bill arrives when you ask for it. Before writing any code I turned that into a product brief with every flow written out end to end, plus the edge cases a builder would otherwise guess at. Two phones at one table, an item that sells out mid-order, a guest who walks out without paying. A short list of assumptions went back to the owner to confirm before any of it was built.

The model

The whole system hangs on one rule: one open tab per table.

The first order from table 12 opens a tab. Every order after that is a new round on the same tab, and anyone who scans the code on table 12 joins it. When someone taps Pay & leave, there's one bill for the table, grouped by round. Staff confirm the payment, the tab closes and the table is free again.

A tab moves through four states: open, bill requested, paid, or closed unpaid. A round moves through new, preparing, ready and served, or gets cancelled. Nearly every feature is a move between two of those states, and the database checks each move.

Three apps, one codebase

Guest

The guest app runs in the phone's browser with no login and no install. Each table has its own QR code, so the table is already known when the menu loads. A generic code on the counter also works. It asks which table you're at, and only offers tables that don't already have an order going. A table that's taken is joined by scanning its own code.

The menu has sticky category chips, a veg or non-veg mark on every item, and sold-out items that stay visible but can't be added. The cart sits in a bar at the bottom and survives a refresh. Once a round is placed, its status updates live as the kitchen works through it.

At checkout the guest picks cash, card or UPI and enters a 10-digit mobile number. With UPI, the button opens their payment app with the cafe's UPI ID, the exact amount and the table number already filled in. Then the screen waits until a server confirms the money arrived and shows Paid, thank you!.

Staff

Each person on shift picks their name and enters a six-digit PIN. The orders board shows tickets in New, Preparing and Ready columns on a tablet and as one list on a phone. Each ticket has the table number in large type, the items with quantities first, any notes, and a timer that turns amber past the cafe's target ticket time. One button moves it to the next step.

A new round or a bill request plays a two-note chime and vibrates the device. Browsers block sound until the page has had a user gesture, so the PIN pad unlocks audio on the first key press, before the first order arrives.

The tables view shows every table in floor order as free, order placed, served or wants to pay, with the open amount. Tapping a table opens its tab. From there staff can cancel a round, confirm a payment, or close the tab as unpaid when nobody is going to pay, without faking a cash payment to clear it.

Admin

The owner signs in with email. The dashboard shows today's revenue against the same weekday last week, order count, average time from order to served, open tabs, the payment mix and active members. The rest of the console covers the menu, tables (with a printable sheet of QR stands), staff accounts and PINs, members, and settings like the GST rate and UPI ID.

Memberships

After the core loop worked, the owner wanted to sell something that wasn't a plate of food. The first one is a yearly engraved cup kept at the cafe. There will be others later, like a discount pass or a free-biscuit card.

Rather than build the cup, I built a small system for defining plans. A plan is a menu item with a few extra fields: how many days it lasts, a discount percentage, a number of free items from a category, and what the cafe has to prepare. Because a plan is a menu item, it goes through the same cart, round, bill and payment as a coffee. There's no second checkout.

When staff confirm payment on a tab that contains a plan, the database creates a membership keyed to the guest's phone number. On a later visit the guest attaches that number to the tab, the discount applies to the bill, and staff see the membership on the ticket and can mark items as free. Free items are zeroed before the discount is worked out, so nothing is counted twice. A free item on a cancelled round gives its credit back.

There's no card on file, so there's no auto-renewal. A membership simply ends on its expiry date, and renewing means buying it again.

How it's built

Vivere is one Next.js 16 app with three route groups, on Supabase for the database, auth and realtime.

The rules live in Postgres. Placing a round, requesting the bill, advancing a ticket, confirming a payment, cancelling a round and closing a tab unpaid are each a database function. Each one checks who's calling and whether the move is allowed, then makes the change in one transaction. Errors come back as short codes like bill_already_requested or item_unavailable:Almond croissant, and the app maps each code to a sentence that tells the guest what to do next.

Guests never create an account. When someone opens a table link, the request is signed in anonymously, so every phone has its own identity without a login screen. That identity is what makes "Phone 2" a member of table 12's tab, and it's what stops a phone at one table from reading another table's bill.

Phone numbers are visible only to staff. Guests can read the tab's status but not the column holding the number, and memberships have no client access at all. Staff and admin reach that data only through database functions that check the caller's role first. Table codes are hidden the same way, so nobody can list them and order to a table they aren't sitting at.

A few failure cases shaped the code more than the features did:

  • Double taps and bad signal. Every round carries an idempotency key. If Place order is retried after a dropped connection, the same round comes back instead of a second one going to the kitchen.
  • Two phones ordering at once. Placing a round takes a lock on the table, so two first orders can't open two tabs. A unique index allows only one live tab per table.
  • Two servers on one ticket. Advancing a round passes along the status the tablet last saw. If someone else already moved it, the second tap fails with a message to refresh.
  • The bill changing at the counter. Confirming a payment checks the amount against the current total. If a round was cancelled in the meantime, the server is told to check the new total before confirming.

Money is integer paise throughout, and GST is worked out in Postgres, so the bill a guest sees and the amount staff confirm come from the same function.

The stack

Next.js 16 with the App Router and Server Actions, React 19 and TypeScript. The interface is Tailwind 4 and shadcn/ui on top of the cafe's own design system: a crimson and cream palette taken from the logo, Titan One for table numbers, and light and dark themes. Supabase provides Postgres, row-level security, anonymous and password auth, realtime, and storage for menu photos. Validation is zod. Tests are Vitest for components and pure functions, a second Vitest suite that runs the database functions against a real Supabase project, and Playwright for the full loop from scan to paid.

What's still needed

These were in the original brief and left out of the first version:

  • Receipts by SMS or WhatsApp. The phone number is collected at checkout, but nothing is sent to it yet.
  • Automatic UPI confirmation. Staff confirm every payment by hand. A payment gateway could confirm UPI on its own.
  • Item options. Sizes and milk choices with price changes aren't modelled. Guests can add a note to any item instead.
  • Day close. Totals by method, cash-drawer reconciliation and a CSV export, along with an audit log of voids and payment confirmations.
  • Moving a tab to another table, and voiding a single item instead of a whole round.

Like the look of Vivere Bay Cafe?

Let's build something →