Files
kooltab-backoffice/docs/architecture-decisions.md
T
brammie15 314717ba3a
CI / build-and-test (push) Canceled after 0s
feat: move to V2
2026-10-29 11:21:52 +01:00

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.