Design

LineUp.

A virtual line-up for restaurants. Two apps, one queue — the merchant stand that runs the line, and the client phone that makes the wait survivable. Every piece below is rebuilt from the app source.

LineUp puts a restaurant's line on two screens instead of a clipboard. A guest scans a code at the door, joins in two taps, and carries their place in line out into the neighbourhood; the host runs the queue from the counter. Nobody has to ask how much longer — both sides are looking at the same number at the same moment. It is live today as a self-initiated project, built end to end and deployed — two installable apps, one edge backend, and notifications that really arrive.

What a guest sees

The code by the door is alive. A new one is minted every 60 seconds, a ring sweeping out the time beside it, so a photo of it texted across town goes stale before it is useful. A spent or expired code answers in a sentence a person can act on — scan the live code at the counter — rather than an error number. Joining is a name and a party size; the app checks you are actually near the venue, and that is the whole form.

Then the screen goes calm on purpose: a dark standby with one number on it — your place in line — a live estimate, and a small walking avocado pacing out the progress. When the host seats a table, that number moves in about 25 milliseconds; you watch the line advance rather than refreshing it. And if the wait drags, the avocado is a toy: tap it and it lifts off the progress bar into an endless runner whose obstacles are hand-drawn pixel-art Ottawa — a bus, a truck, a Canada goose, Parliament.

Two moments have to reach a phone that is locked in a pocket: you're next and your table's ready. Both arrive as real push notifications — screen off, app closed. (On iOS that only works for an installed app, so the client notices and says "add to Home Screen first" instead of failing silently.) Leaving the line, meanwhile, is a slide rather than a button: it cannot be hit by a stray thumb, and it snaps back if you hesitate.

What the host sees

A tablet dashboard sized for arm's length: tall tappable rows, one per party, each carrying the name, the size, the wait so far, and exactly one next action — Notify, Seat, No-show. A stat strip keeps the day's shape in the corner of the eye — in line, average wait, seated today, no-shows — and when tables are turning slowly, the host raises the base wait from settings and every guest's estimate updates as they watch. The other face of the app is the full-screen QR display with its countdown ring, pointed at the door.

Below about 900 pixels the same app reshapes into a phone UI — the queue folds into denser rows, the stats into a three-up strip, the actions into a bottom bar — because plenty of hosts run the line from a doorway with a phone in one hand. The component shelf above shows both renderings, each at its real size.

How it works

Two React PWAs talk to one Cloudflare Worker, and each venue's queue lives in its own Durable Object — a single authority for that line, so two guests joining in the same instant can never scramble the order. The object holds both apps' live WebSockets, rotates the QR token on a 60-second alarm, and persists to D1, a schema of three tables. The push notifications are 131 dependency-free lines of WebCrypto, written from scratch because push libraries assume Node and a Worker isn't Node. All told, 8,065 lines across 68 files.

Fig. 1How it fits together — one venue, end to end
Merchant stand React PWA · tablet + phone
Guest phone React PWA · installable
Worker API gateway
Queue authority strict join order
WebSocket hub guest + merchant channels
60 s alarm QR token rotation
D1 · SQLite three tables
Clerk merchant auth
Web Push browser push service
Every request routes to that venue's single object: it orders the queue, holds both apps' sockets, rotates the QR on an alarm, and writes rows to D1. Web Push reaches the phone when the socket can't.
Fig. 2Join → seated, one guest's pass through the system
Guest phone Worker Venue object D1 Merchant stand 1 join with rotating QR token 2 route via idFromName(venue) 3 validate token — 120 s life, 2 uses 4 insert party 5 WebSocket — queue snapshot (~25 ms) 6 WebSocket — position + live ETA 7 Notify party (via Worker) 8 Web Push — “you’re next”, screen off 9 Seat party (via Worker) 10 update party + day stats 11 WebSocket — table’s ready → seated
The two dashed returns are the product's whole promise: the queue snapshot that keeps both screens on the same number, and the push that reaches a phone in a pocket.

The speed is measured rather than claimed — the API is public, so these are real samples against the deployed URL, timed from a laptop whose nearest Cloudflare colo is Montreal:

Response time, 25 samples each, sorted ascending
Worker only Worker → Durable Object → D1
Sorted, so the shape reads as a distribution rather than as jitter. Drag across to read both lines.
Pathp50p95Worst
Worker only22 ms30 ms38 ms
Worker → object → D1, warm50 ms59 ms69 ms
Worker → object → D1, cold299 ms386 ms426 ms
WebSocket ping → tick25 ms—36 ms

The single object costs about 28 ms over a bare Worker, and buys strict ordering plus a broadcast that it fans out itself; once a socket is open, an update lands in 25 ms — the difference between an app that feels live and a page that polls. The wait estimate itself is kept deliberately simple — a base the host controls, plus seven minutes per group ahead — because a legible number that moves the moment something happens beats a clever one that arrives late. A real venue would fit that slope from its own event log, which the schema already keeps.

One design language, two densities

Both apps import the same tokens and the same primitives — stepper, lined field, pill, stat, progress, switch, wordmark — and every token is defined once. The difference in feel is density alone: the merchant stand runs 64-pixel rows and a four-up stat strip; the client app spends its space on one 156-pixel number and a lot of warm paper. One blue accent does every job that needs emphasis — green appears exactly once, on the seated confirmation, and red exactly once, on leaving. The whole system is 149 lines of shared tokens. Hand-written CSS, hand-rolled primitives.

The client also ships its sense of humour: thirteen Lottie walking characters at 278 KB of JSON, against nine pixel-art Ottawa sprites for the runner game that total 1,702 bytes. 278 KB of walking vegetables next to 1.7 KB of Ottawa is still the most honest summary of this project available.

How far it goes

It stops at a demo. The QR admits 120 joins an hour, sized for a café rather than a stadium; the location gate is switched off in production so the demo stays openable; and the wait math is a straight line on purpose. Writing this page also turned up a real bug — an idle heartbeat that quietly kept every venue that ever existed warm, running queries to tell nobody nothing — which is its own small argument for writing a project up after building it.

Walker artwork by Jeffrey Christopher.