Skip to content

Hospitality POS that keeps working when the internet doesn't

A venue system built as a desktop terminal that owns its own data, so tabs stay open, dockets keep printing, and a dropped line is an inconvenience instead of a closed bar.

TablineThe Salt Loft·Bar 1·PriyaLine down 18 min · serving from this terminal21:23
Floor · Lounge8 tabs open6 tables changed offline
Bar counterLoungeTerraceOpen tabSettlingFree Queued to sync
1 2
Reyes$46.00 1h 12m
2 4
Free
3 4
Lindqvist$118.50 2h 03m
4 2
Moreau$62.00 1h 40m
5 6
Birthday · Nair$234.00 2h 51m
6 2
Free
7 4
Hale$72.00 21m
8 4
Free
9 2
Walk-in$26.00 34m
10 4
Osei$98.00 1h 55m
11 2
Free
12 6
Okafor party$182.50 3h 52m
On the floor$839.00Covers seated31Since the line dropped21:05 → now · 14 rounds, 3 settlementsNew tab
Okafor party · Table 12Open
Opened 17:31 at Bar 1 · Visa ···· 4418 pre-auth Moved to Table 12 at 19:15 · 6 guests Rounds from 3 staff on 3 terminals
R13 × Harbor Pale · 1 × Spritz17:31 · Bar 1 · Priya$25.50Happy hour
R22 × Negroni · 2 × Dry Martini17:58 · Bar 2 · Marcus$42.00Happy hour
R32 × Loft burger · 2 × Fries · 1 × House red19:15 · Floor · Dee$63.00
R43 × Harbor Pale · 2 × Negroni20:40 · Floor · Dee$52.00
On this tab since 21:05Split preview saved · 6 guestsService charge note addedCard pre-auth kept, not captured
Tab total$182.50Written to this terminalnot waiting on the server
MoveSplitSettle
salt-loft-bar1.dbQueued to sync41 changesLast sync21:05:02Printers3 of 3 drainingTerminals here4 · all serving

How the work was scoped

Industry

Restaurants & Hospitality

Duration

26 weeks

Cooperation model

Dedicated team

Services
Desktop engineeringVenue operationsStock and recipes
Integrations
Stripe TerminalEpson TM printersXeroDeputy
Technologies
ElectronNext.jsTypeScriptPrismaSQLiteNode.jsWebSocket
Team
1 Project lead2 Desktop engineers1 Backend engineer1 Product designer

Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.

The problem

The hard problem

A tab isn't a shopping cart. It stays open for hours, moves between tables, splits across six people, and re-rates itself when happy hour ends. Their previous system modeled a sale as a single moment, so every one of those was a workaround. And when the line dropped, the bar stopped.

The deeper fault was the data model, more than the network. Their system represented a sale as one moment, and a tab is nothing like that: it opens at the bar, moves to a table, absorbs rounds from three staff, splits six ways and settles hours later. Every one of those was a workaround, and each workaround was a place where the connection mattered again.

A desktop terminal with a real database on the machine, not a cache. Floors and tables, tabs that stay open, dockets queued to the right printer, and stock that depletes by recipe. The server is where terminals meet, not where the truth lives.

Introduction

The system we were asked to build

Four bars and a restaurant ran on registers that assumed a working connection, and on a Friday night that assumption cost them service. We built a terminal that keeps its own database and treats the network as an optional extra.

Four bars and a restaurant under one operator, all running registers that assumed a working connection. On a Friday night that assumption stopped service. Not degraded it: stopped it. The engagement began after a Saturday when the line dropped at nine and one venue took cash on paper for two hours, then spent Sunday reconstructing the evening from receipts.

Desktop & Platform Engineering

The solution

How the pieces fit

  1. 01

    Modeled the venue first (floors, tables and tabs) before anything about payment

    Two weeks of running service alongside their staff came before a screen was drawn, which is where the tab-as-a-sale problem surfaced.

  2. 02

    Gave each terminal its own database file, so service never depends on a link

    Each terminal holds its own database file, a real one and not a cache, so a dropped line is an inconvenience for syncing and never for serving.

  3. 03

    Queued dockets per printer, so a jammed printer delays one station and not the round

    Every line knows the station that makes it and each printer drains its own queue, so a jam holds that station's tickets and reprints on clear.

  4. 04

    Depleted stock through recipes, so a poured cocktail moves five ingredients

    Recipes were captured from the bar team over two weeks, and the exercise itself found four drinks being poured to two different specifications.

  5. 05

    Reconciled terminal clocks on reconnect, so a late arrival can't reorder history

    Clocks reconcile on reconnect and disagreements are logged, so a terminal rejoining late can't reorder the evening's history.

Tabs that behave like tabs

Open at the bar, move to a table, split any way the table wants, and settle whenever.

A tab is a first-class record with its own lifecycle, so it can open at the bar under a name or a card pre-auth, move to a table, absorb rounds from three staff on three terminals, and settle hours later. Splitting is arbitrary (by item, by share, by amount), and a split that doesn't reconcile refuses instead of silently rounding. Nothing about the tab depends on the server being reachable.

What shipped
  • Tabs are records, not unpaid orders: they move and merge
  • Arbitrary splits by item, share or amount
  • A split that doesn't reconcile refuses instead of rounding
TablineThe Salt Loft·Floor·DeeOnline · synced 21:4121:41
Okafor party · Table 12T-4471
Opened17:31 · Bar 1Visa ···· 4418 pre-authMoved19:15 · bar → Table 12Same record, same roundsRounds4 from Priya, Marcus, DeeBar 1 · Bar 2 · Floor
Round 117:31 · Bar 1 · Priya$25.50
3 ×Harbor Pale, pintHappy hour$16.50
1 ×SpritzHappy hour$9.00
Round 217:58 · Bar 2 · Marcus· Started 17:58 · completed 18:02$42.00
2 ×NegroniHappy hour$20.00
2 ×Dry MartiniHappy hour$22.00
Round 319:15 · Floor · Dee· Moved from the bar to Table 12$63.00
2 ×Loft burger$38.00
2 ×Fries$14.00
1 ×House red, glass$11.00
Round 420:40 · Floor · Dee$52.00
3 ×Harbor Pale, pint$24.00
2 ×Negroni$28.00
Tab total$182.50
Settled so far$116.50
Outstanding$66.00
Split this tab3 of 6 settled
By item By share By amountMix modes per guest
$116.50 settled of $182.50$66.00 to go
AdaBy item · Burger, fries, house red$37.00Paid 21:38
BenBy amount · Fixed amount$40.00Paid 21:40
CleoBy amount · Fixed amount$39.50Paid 21:41
4Guest 4By share · 1 of 3 of the rest$22.00Due
5Guest 5By share · 2 of 3 of the rest$22.00Due
6Guest 6By share · 3 of 3 of the rest$22.00Due
Split refused · nothing rounded$25.00 + $25.00 + $20.00 = $70.00 against $66.00 outstanding, $4.00 over.
Undo last shareTake Guest 4 · $22.00
salt-loft-floor.dbTabT-4471 · open 4h 10mQueued to sync0
On screen

An open tab mid-settle: opened at the bar under a card pre-auth, moved to Table 12, four rounds from three staff on three terminals, three shares already paid by item and amount, and a split that didn't reconcile refused instead of rounded.

TablineHarbor Room · Kitchen display3 printers drainingGrill holding 420:14
GrillPRN-GRILL-014
Paper jam · queue pausedHolding 4 · reprints on clear
T4Mains fire 20:09Held
2 × Ribeye, medium1 × Loft burger
T9Mains fire 20:11Held
1 × Half chicken1 × Loft burger, no onion
T2Mains fire 20:12Held
3 × Lamb skewers
T15Mains fire 20:13Held
2 × Ribeye, rare
FryPRN-FRY-013
DrainingOwn queue · unaffected by grill
T4Mains fire 20:09Printed
2 × Fries1 × Onion rings
T2Mains fire 20:12Printed
2 × Fries
T15Mains fire 20:13Printing
1 × Fries, large
ColdPRN-COLD-013
DrainingOwn queue · unaffected by grill
T9Starters fire 20:11Printed
2 × Oysters, half dozen
T11Starters fire 20:12Printed
1 × Crab toast1 × Burrata
T7Dessert fire 20:14Queued
2 × Lemon posset
BarPRN-BAR-023
DrainingOwn queue · unaffected by grill
T4Drinks fire 20:10Printed
2 × Negroni1 × House red
T9Drinks fire 20:12Printed
1 × Dry Martini1 × Spritz
T15Drinks fire 20:14Printing
2 × Harbor Pale
All dayRibeye 4Loft burger 2Fries 5Oysters 2Negroni 213 dockets · routed by station · course and fire time on every ticket
On screen

The kitchen display with dockets queued per station: the grill printer jammed and holding its four tickets to reprint on clear, while fry, cold and bar keep draining, every ticket carrying its course and fire time.

Dockets per station

Each line goes to the printer that makes it, queued independently so one jam never stops the kitchen.

Every line knows the station that makes it, so a docket goes to the grill printer, the fry printer or the bar independently. Each queue drains on its own: a jammed printer holds its own tickets and reprints on clear, while the rest of the kitchen keeps working. Dockets carry the course and the fire time, so a kitchen sees sequence instead of a stream of items.

What shipped
  • Lines routed to the station that makes them
  • Per-printer queues: one jam never stops the kitchen
  • Course and fire time on the docket, so sequence survives
TablineThe Salt Loft·Bar 2·MarcusOnline · synced 21:4121:41
Round 2 across the 18:00 boundaryHappy hour 16:00–18:00
Happy hour window · Mon–Fri16:0016:3017:0017:3018:0018:3019:00Round 2 · started 17:58, completed 18:02 →2 × Dry Martini rung 18:02 · round’s windowSettled 21:41 · re-rated at settle
Round 2 at settle · Bar 2 · MarcusSettled 21:41
LineRung inWindowListCharged
2 × Negroni17:58Round from 17:58$28.00$20.00
2 × Dry Martini18:02Round from 17:58$30.00$22.00
Priced at the ring-in moment instead
2 × Dry Martini at 18:02: full price$30.00
Someone voids and re-keys at $22.00$8.00 difference
Round 2$42.00 No void, no re-key
Price rules on this terminalCalendar
Happy hour · v4Mon–Fri · 16:00–18:00Draft, wine, house cocktailsApplied to R2
Late bar · v2Sun · 21:00–23:00House cocktails onlyNot today
Staff drink · v1After closeOne per shift, loggedNot today
Rules are evaluated here, from this terminal’s own copy. Happy hour applies with the line down.
salt-loft-bar2.dbTabT-4471 · Okafor partyPrice rulesHappy hour v4 · evaluated on this terminal

Happy hour that re-rates itself

On screen

A round started at 17:58 and completed at 18:02, re-rated when the tab settled: both lines priced by the round's happy-hour window, whatever moment each was rung in, with the rule evaluated on the terminal itself.

Prices follow the happy-hour window the item belongs to, so nobody retypes a round.

Prices resolve against the window the item belongs to, whatever the instant it was rung in, so a round started at 17:58 and completed at 18:02 re-rates itself when the tab settles. Nobody has to void it and retype it. The rules are calendar-driven and evaluated on the terminal, which means happy hour still works when the connection doesn't.

What shipped
  • Prices resolve against the window, not the ring-in moment
  • Rounds spanning the boundary re-rate at settle
  • Rules evaluated on the terminal, so they survive an outage
TablineThe Salt Loft·Office·RhianOnline · ledger current23:48
Tonight's recipesVersioned
Dry Martiniv3 since 09/0238 poured · 3 ingredients
London dry gin50ml× 38
Dry vermouth25ml× 38
Lemon twist1× 38
v2 poured 60ml gin until 09/01 · earlier nights keep their pour
Harbor Sourv2 since 08/1444 poured · 5 ingredients
Bourbon50ml× 44
Lemon juice25ml× 44
Sugar syrup20ml× 44
Egg white15ml× 44
Aromatic bitters2 dash× 44
Negroniv1 since 07/2061 poured · 3 ingredients
London dry gin25ml× 61
Red bitter25ml× 61
Sweet vermouth25ml× 61
Variance · Fri 09/111 over tolerance
Recipes poured412Ingredients moved1,187Spirits over8.0%
CategoryExpected useActual useVariance
Spirits11.92 L12.87 L+8.0%
Vermouth & bitters3.40 L3.46 L+1.8%
Wine18.75 L18.98 L+1.2%
Draft beer96.0 L96.9 L+0.9%
Juice & syrups4.10 L4.12 L+0.5%
Garnish212214+0.9%
Spirits by ingredient
London dry gin4.30 L4.71 L+9.5%
Bourbon3.12 L3.33 L+6.7%
Other spirits4.50 L4.83 L+7.3%
CountFri 09/11 close · counted 23:30Depletionby recipe ingredientTolerance3.0%
On screen

The night's count by recipe: a Dry Martini depleting 50ml of gin, 25ml of vermouth and a twist, a five-ingredient sour, versioned pours, and spirits running eight percent over, visible the same night.

Recipe-level stock

A poured drink moves its ingredients, so variance is visible the same night.

A cocktail depletes 50ml of gin, 25ml of vermouth and a garnish, so stock reflects what was actually poured. Variance shows up the same night instead of at the monthly count, and a bar running eight percent over on spirits becomes a conversation on Tuesday, not a discovery in four weeks. Recipes are versioned, so a changed pour doesn't rewrite history.

What shipped
  • Depletion by recipe ingredient, not by unit sold
  • Variance visible the same night, not at month end
  • Recipes versioned, so a changed pour doesn't rewrite history

Multi-venue transfers

Stock moves between venues against one ledger, so a keg leaving one bar arrives in another's count.

Transfers between venues move against one ledger, so a keg leaving one bar is in the other's count before it arrives, with no one left to remember to adjust both. Each venue's terminals hold their own database and reconcile through the ledger, which means group-level stock is derived from it, never maintained by hand, and two venues can't disagree about where something is.

What shipped
  • Inter-venue transfers written to one shared ledger
  • Group stock derived, never separately maintained
  • Two venues can't disagree about where a keg is
TablineQuayside Group·Group office·TomOnline · 4 of 5 venues syncing17:30
SLThe Salt LoftBar · 4 terminalsSynced
CGCopper GantryBar · 3 terminalsSynced
NQNorth Quay TapBar · 3 terminalsTrading offline · 23 queued
LLLow LanternBar · 2 terminalsSynced
HRHarbor RoomRestaurant · 5 terminalsSynced
Group ledgerOne ledger · 5 venues
SeqTimeVenueEntryItemQty
8841216:20Copper GantryTransfer outHarbor Pale, 50 L keg−2
8841316:20The Salt LoftTransfer inHarbor Pale, 50 L keg+2
8842016:48Harbor RoomDeliveryHouse red, 9 L case+6
8843117:05Low LanternDepletionLondon dry gin, ml−1,450
8843717:12The Salt LoftDepletionHarbor Pale, ml−11,360
8844217:20Copper GantryCountBourbon, 700ml14
queued17:26North Quay TapDepletionHarbor Pale, ml−8,520
North Quay Tap’s entries are held on its terminals and take a sequence on reconnect.
Transfer TR-0917In transit
2 × Harbor Pale, 50 L keg
FromCopper Gantry−2 at 16:20ToThe Salt Loft+2 at 16:20
Both sides written as one entry pair, before the van arrives
Group stockDerived · never stored
ItemSLCGNQLLHRGroup
Harbor Pale, 50 L keg6342116
London dry gin, 700ml181197651
Bourbon, 700ml121485443
House red, 9 L case2111914
Dry vermouth, 750ml5432317
Group column is the sum of the ledger, so no two venues can disagree about a keg.
Ledgernext seq 88443Group stockderived from the ledgerNorth Quay Taptrading offline · 23 queued
On screen

Five venues on one ledger: two kegs moving from Copper Gantry to The Salt Loft, written to both counts before they arrive, group stock derived from the ledger, and North Quay Tap trading offline with its changes queued.

Process

Phase by phase

  1. Phase 1: Venue Modelling

    Floors, Tables and the Life of a Tab

    Sat through service at two venues to map how a tab actually behaves (opened at the bar, moved to a table, split at the end) and modeled that before any screen was drawn.

    • Venue Domain Model
    • Tab Lifecycle Specification
    • Floor Layout Editor Brief
  2. Phase 2: The Terminal

    A Desktop Application That Owns Its Data

    Built the terminal as a desktop application over a local database file, so the register is authoritative for its own service and the network is something it uses, never something it needs.

    • Desktop Terminal Build
    • Local Schema and Migrations
    • Packaged Installer
  3. Phase 3: Dockets and Stations

    Print Queues Per Station

    Routed each line of an order to the station that makes it, and queued per printer so a jam at the grill holds the grill's dockets and nothing else.

    • Docket Routing Rules
    • Per-Printer Queue
    • Reprint and Void Handling
  4. Phase 4: Stock by Recipe

    Ingredients, Not Units

    Made stock depletion follow the recipe, so pouring a cocktail moves every component, and transfers between venues reconcile against the same ledger.

    • Recipe Model
    • Inter-Venue Transfers
    • Variance Report
  5. Phase 5: Reconnection

    Clock Drift and Ordered Replay

    Replayed queued work in the order it happened, whatever order it arrived in, and logged terminals whose clocks disagreed so a drifting machine is found before it corrupts a night's history.

    • Replay Ordering
    • Clock Reconciliation Log
    • Conflict Review Screen
TablineThe Salt Loft·Bar 1·PriyaReconnected 21:24 · replaying21:24
Replay · tab T-4502 · Hale, Table 7Ordered by event time
Queued changes from three terminals, placed in the order they happened. Arrival order is shown for comparison only.
#HappenedTerminalChangeArrived
121:02:10Bar 1Open tabHale · card pre-auth · 2 × Harbor Pale $16.00live
221:06:40FloorMove tabBar → Table 721:24:03
321:08:5021:11:30Service barAdd round4 × Negroni $56.0021:24:06
421:10:05Bar 1Split tab3 shares, even21:24:01
521:12:20FloorSettle shareShare 1 of 3 · $24.0021:24:03
Tab reconciles · $72.00 in three shares of $24.00The round lands before the split, as it did at the bar.
Terminal clocks on reconnect1 logged
Bar 1salt-loft-bar1.db+0.4sWithin
Bar 2salt-loft-bar2.db−1.1sWithin
Floorsalt-loft-floor.db+0.8sWithin
Service barsalt-loft-servicebar.db+2m 40sLogged
Service bar ran +2m 40s fastIts round stamped 21:11:30 happened at 21:08:50. Replayed at the corrected time; the machine is logged for review before it drifts further.
Conflict review0 openClock log
Outage21:05–21:24Replay orderwhen it happenedClock disagreements1 logged
On screen

Reconnection after the outage: one tab's queued changes from three terminals replayed in the order they happened, and the Service bar terminal's fast clock corrected and logged for review.

What it carries now

Uninterrupted

Service continuity

−58%

Time to settle a split tab

−41%

Stock variance

5

Venues on one system

Service continuity is a statement about outages since cutover: the line has dropped and service hasn't. Time to settle a split tab is measured from the register's own timestamps at comparable trading periods. Stock variance compares recipe-level depletion against physical counts, before and after, a like-for-like the previous unit-level system couldn't produce.

Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.

What the architecture settled

  1. 01

    A tab is a long-lived object, not a transaction. Model it as a sale and every real behavior becomes a workaround.

    A tab is a long-lived object with a lifecycle, and modeling it as a transaction turns every genuine bar behavior into something the staff have to work around.

  2. 02

    A terminal that owns its data doesn't need the network to be reliable, only to be present eventually.

    The network still matters for reconciliation and for the other terminals; what changed is that it stopped mattering for taking an order.

  3. 03

    Queue per printer, not per order: one jammed station should never hold a whole round.

    Per-printer queues over per-order ones is a small decision with a large consequence: one jammed station should never hold an entire round.

  4. 04

    Replaying by arrival order silently corrupts history. Order by when it happened, and log the clocks that disagree.

    Replaying by arrival order corrupts history silently. Ordering by when things happened, and logging the clock disagreements, is what makes the reconstruction trustworthy.

How the work was run

We ran service alongside their team at two venues before drawing a screen, which is where the tab-as-a-sale problem surfaced. The terminal shipped venue by venue, with the busiest bar last. By the time it cut over, the queue and replay behavior had already survived four quieter Fridays.

A dedicated team for twenty-six weeks, running service alongside the venue staff at two sites before drawing a screen. That's where the tab-as-a-sale problem surfaced. The terminal shipped venue by venue with the busiest bar last, so by the time it cut over, the queue and replay behavior had already survived four quieter Fridays.

Five changes, three orders

Replay by arrival and the night quietly rewrites itself.

One tab collects a move, a round, a split and a payment from three terminals while the line is down. Here are the same five changes replayed in three orders against the same rules. Switch tabs, or use the arrow keys once one is focused.

Queues are applied in the order they reach the server. Bar 1 reconnects first, so its split is applied while the tab still only holds two pints.

Line down 21:05–21:24 · 5 changes, 3 terminals · applied in this order

  1. 121:02:10Open tab · Bar 1Hale · card pre-auth · 2 × Harbor Pale $16.00Tab opened at $16.00
  2. 221:24:01Split tab · Bar 13 shares, even$16.00 does not split evenly: refused
  3. 321:24:03Move tab · FloorBar → Table 7Moved to Table 7
  4. 421:24:03Settle share · FloorShare 1 of 3 · $24.00No $24.00 share to settle: to review
  5. 521:24:06Add round · Service bar4 × Negroni $56.00Tab now $72.00

Tab T-4502 · Hale, Table 7

Tab total$72.00
Splitnone
Settled$0.00
Refused or to review2

History reordered

The split was taken against a tab that didn't yet hold the round, so it refuses, and the payment has nothing to settle against.

Illustrative: times, amounts and the replay speed are drawn for this example. The rule is the product’s: order by when it happened, and log the clocks that disagree.

Architecture

From a round at the bar to one ledger across five venues

The first three stages run on the terminal in the room and never wait on the line. The last two need the network only to be present eventually: the server is where terminals meet, not where the truth lives.

  1. 01 · Trigger
    A line rung in on a tabA tab is a record with its own lifecycle, so it opens, moves, splits and settles without the server being reachable.
  2. 02 · Storage
    The terminal's own database fileA real database on the machine, not a cache. A dropped line is an inconvenience for syncing, never for serving.
  3. 03 · Engine
    Pricing, dockets and recipes, on the terminalHappy-hour rules evaluate locally, each printer drains its own queue, and stock depletes by recipe ingredient.
  4. 04 · Queue
    Ordered replay on reconnectQueued work replays in the order it happened, whatever order it arrived in, and clock disagreements are logged.
  5. 05 · Delivery
    One ledger, shared by every venueTransfers write to one ledger and group stock is derived from it, so two venues can't disagree about a keg.
On the terminal, no line needed Waits for the line, then catches up

What happens when the connection goes

No more cash on paper, and no Sunday spent rebuilding Saturday

The line drops; service does not

Each terminal holds its own database file, and tabs, splits, happy-hour pricing and dockets all run from it. The network matters for reconciliation and the other terminals. Taking an order never waits on it.

One jammed printer holds one station

Every line is routed to the station that makes it, and each printer drains its own queue. A jam at the grill holds the grill's tickets and reprints them on clear; the rest of the kitchen keeps working.

A late terminal can't rewrite the night

Queued work replays in the order it happened, whatever order it arrived in. Terminal clocks are reconciled on reconnect and disagreements logged, so a drifting machine is found before it corrupts the history.

Running venues that cannot stop serving when the line does? Scope your build in 3 minutes.

Scope your build
Have a project?

Let's talk

Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.