Skip to content
App Crafters
All projects

A reference build: we made this ourselves to show how we work. It is not a client engagement, there is no customer to name, and there are no measured outcomes to report - so this page does not pretend to have any.

Scheduling against tables instead of covers

A reference build: a table allocator that assigns each booking to the tightest table that fits, combines adjacent tables when it has to, and reports the seats a decision left empty.

Client
Reference build
Sector
Restaurant group
Year
2026
Built with
Next.js, TypeScript, Postgres, Viber

The numbers

4
Turn-time bands derived from party size, because one length either loses the late sitting or holds tables empty
2
Combinable pairs on this floor, and the allocator reaches for them last rather than first
1
Number nobody was measuring: seats left empty per booking

The problem

A group with three rooms takes bookings through a widget that counts covers against a limit. So a party of two gets a six-top on a Friday because the cover count allowed it, and a party of eight is refused while two adjacent four-tops sit empty, because the widget has no concept of pushing tables together. Both are decisions made at the moment of booking, which means no amount of reporting afterwards recovers them.

What we did

  1. 1

    Turn time is derived from party size rather than set once. A single turn length applied to every booking either loses the late sitting or holds tables empty, and which of the two it does depends on where the number was set - so it is not a setting, it is a curve.

  2. 2

    Assignment is tightest-fit first and combinations only as a fallback, because pushing two tables together costs the flexibility of having two. The allocator will take a worse fit rather than break up a pair it might need later.

  3. 3

    A refusal names the constraint that caused it. "Fully booked" is useless to a manager; "tables three and four together would seat them, but combining is switched off" is a setting somebody can change while the guest is still on the phone.

  4. 4

    Seats wasted is surfaced per booking and totalled per service, because it is the number that makes the case for the whole system and nobody was measuring it.

Where it got to

The allocator on the restaurants page is this logic, running in your browser with six tables and one service. Switch combining off and try a party of eight to see the refusal that matters.

A worked example

The allocator on the restaurants page is this logic. One Friday service, six tables, combining off then on:

Covers booked at the start
22
Floor in use
26%
Party of 8 at 21:30, combining off
refused
Reason
tables 3 + 4 would seat them
Same booking, combining on
30 covers, 40%
Two more pairs at 22:00
34 covers, 4 seats empty, 48%

The second pair took a six-top because nothing tighter was free, and the allocator said so. A widget that counts covers reports none of this.