2.4 KiB
Architecture decisions
1. The server holds the tabs; terminals send events
Terminals record what happened as events and replace their state with the server's on every sync. Two terminals and
the dashboard can change one tab without overwriting each other, and an offline terminal catches up by sending its
outbox. The terminal's events are kept (TabEvent) so each is applied once.
2. One drink is one row
TabUnit is a drink, on an open tab or in a settlement. Settling moves rows instead of copying JSON, the database
checks that a drink is in exactly one place, and sales reports are SQL. A venue's open tabs are loaded whole under a
row lock on the venue (tens of tabs, not thousands), changed in memory with pure rules, and written back as a diff.
3. Per-field last write wins for products
A product edited on the dashboard and on an offline terminal keeps both edits: every field remembers when it was last changed, and a change only wins over an older one of the same field.
4. One contract
The terminal protocol and the dashboard API are zod schemas and types in @kooltap/shared. The API validates with
them, the dashboard and the API are typed by them, and the terminal protocol is exported as JSON Schema for the
Flutter app.
5. Sessions in httpOnly cookies
The API sets the access token (15 minutes) and the rotating refresh token (30 days) as httpOnly, SameSite=Lax
cookies, so no script on the dashboard can read them. The dashboard and the API must share a site
(kooltap.com and api.kooltap.com). Every request looks the user up, so a deleted, demoted or signed-out user loses
access at once.
6. Invalidation-only live updates
The API pushes topic names (tabs, stock, …) over Socket.IO; the dashboard refetches that data over the API, and
polls only while the socket is down. A socket can't see anything its owner couldn't fetch.
7. Pictures on disk, by content hash
A picture is stored once, at a path that never changes, so it is cached forever. The database only holds its size; the files live on a volume next to it.
8. Explicit payment method
The dashboard never defaults to a payment method: settling a tab means choosing one.
9. In-memory rate limiting
Fine for one API instance; the throttler can move to Redis without changing controllers. Terminal syncs are not rate limited: the till must never be locked out by other traffic from the venue's IP.