How it works
Designed from the assumption that the device is offline.
Not a cache bolted onto an online till. Every endpoint in the contract starts from the premise that a device will lose its connection, accumulate sales, and reconcile later — sometimes hours later.
The four mechanisms
- 01
The device mints the identifier
The failure it preventsA retry after a lost response creates a second sale.
Anything a till can originate gets a time-ordered UUIDv7 chosen on the device, before anything is synced. Submission is therefore a PUT to an identifier the server has possibly never seen, which is idempotent by construction: the retry targets the same record instead of creating a duplicate.
- 02
Every mutation carries an idempotency key
The failure it preventsOn a bad connection you cannot tell a lost request from a lost response.
Without a key, a client has two choices and both are wrong: risk charging twice, or drop a real sale. With one it retries as often as it likes. The first request wins and every replay is answered with that same stored response.
- 03
State is pulled from a change feed
The failure it preventsRe-downloading the catalogue every morning does not scale and misses deletions.
A device holds a cursor and asks what has changed since. Deletions come back as tombstones rather than as an absence it has to infer. A till that has been in a cupboard for a fortnight catches up in one pass.
- 04
Writes are guarded by versions
The failure it preventsTwo managers edit the same product and the slower one silently wins.
Every resource carries a version, surfaced as an ETag. An update sends the version it last saw and is refused if anything moved underneath it. Last-write-wins is the wrong default when two people can edit the same thing from different rooms.
Which way data flows
Each resource is authoritative in exactly one direction.
This is a contract, not a convention. It is what makes reconciliation a mechanical process instead of a judgement call.
| Resource | Authority | Why |
|---|---|---|
| Catalogue | Server → device | Two tills editing prices offline would diverge with no principled way to reconcile them. Devices pull products, categories and tax rates. They never push them. |
| Sales | Device → server | An order is a financial record, so it is append-only and the server never modifies one. Corrections happen by voiding and re-issuing, which leaves the mistake visible. |
| Stock | Both ways | Written only as signed adjustments, never absolute levels. Two offline tills each selling three of an item must subtract six, and absolute writes cannot express that. |
| Tabs and tables | Shared, versioned | A tab is opened on a handheld at a table and paid at the bar, so it is a server resource. The unnamed sale at the counter never leaves the device that is ringing it up. |
Push, and the poll behind it
Devices are told when to sync. The poll is the backstop.
A server-sent event stream tells each device there is something to pull. The events carry no data — every one of them means onlygo and read the feed
— so nothing about a shop travels that way and the stream is disposable.
The notification is raised inside the same database statement that records the change, so it cannot fire for a write that was rolled back. And devices keep polling every five minutes while the stream is up, because a transport that fails silently is otherwise indistinguishable from a quiet shop.
When two devices disagree
Decided by version, not by luck.
A tab carries the version its device last saw, and a write against a stale version is refused. A till holding unsent changes ignores the feed's older copy of the same tab rather than letting it overwrite the round that was just rung up.
And a tab paid for elsewhere while this till was adding to it is kept rather than dropped. Better a tab somebody has to dismiss than a round nobody can account for.
The trade-off, stated
Stock never blocks a sale.
A merchant would rather not sell what they do not have, and fresher stock data is better than stale. But a till must keep trading when the server is unreachable, and it is not close which of those wins.
So an order arriving with insufficient stock is always accepted, and the oversell is recorded as the real event it is. Stock is a reporting problem. Refusing a customer is a trading problem.
Early access
Be one of the first shops on it.
No forms, no sales team. Tell us what you run, where you trade and what your till does now. The people building Tender read every email and reply themselves.