@@ -1,49 +1,50 @@
|
||||
# Architecture Decisions
|
||||
# Architecture decisions
|
||||
|
||||
## 1. Registration Code Model
|
||||
## 1. The server holds the tabs; terminals send events
|
||||
|
||||
**Decision:** Added a `RegistrationCode` model to the database schema.
|
||||
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.
|
||||
|
||||
**Rationale:** The specification requires a registration-code mechanism. Rather than overloading an existing model or implementing a complex external system, a simple `RegistrationCode` table with venue scoping, expiration, and single-use semantics provides a clean implementation.
|
||||
## 2. One drink is one row
|
||||
|
||||
## 2. Payment Method Must Be Explicitly Selected
|
||||
`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.
|
||||
|
||||
**Decision:** The UI never defaults to `cash`. The user must explicitly select a payment method.
|
||||
## 3. Per-field last write wins for products
|
||||
|
||||
**Rationale:** The specification explicitly forbids silent defaults. This prevents accidental charges and ensures compliance with the business requirement.
|
||||
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.
|
||||
|
||||
## 3. Cursor Implementation
|
||||
## 4. One contract
|
||||
|
||||
**Decision:** Opaque cursor encoded as base64url containing timestamp + UUID tiebreaker.
|
||||
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.
|
||||
|
||||
**Rationale:** Provides a simple, privacy-preserving cursor that allows the server to determine which commands the device has already seen without exposing database internals.
|
||||
## 5. Sessions in httpOnly cookies
|
||||
|
||||
## 4. Refresh Token Storage
|
||||
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.
|
||||
|
||||
**Decision:** Refresh tokens are stored hashed (SHA-256) in a `RefreshToken` table, supporting revocation and rotation.
|
||||
## 6. Invalidation-only live updates
|
||||
|
||||
**Rationale:** Provides security (tokens can be revoked) and aligns with OAuth2 best practices for refresh token management.
|
||||
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.
|
||||
|
||||
## 5. PostgreSQL Everywhere
|
||||
## 7. Pictures on disk, by content hash
|
||||
|
||||
**Decision:** PostgreSQL is used in development, CI and production.
|
||||
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.
|
||||
|
||||
**Rationale:** One database means one set of migrations; the schema uses JSON columns and serializable transactions that SQLite doesn't support the same way.
|
||||
## 8. Explicit payment method
|
||||
|
||||
## 6. Invalidation-Only Live Updates
|
||||
The dashboard never defaults to a payment method: settling a tab means choosing one.
|
||||
|
||||
**Decision:** The API pushes topic names (`tabs`, `commands`, ...) over Socket.IO; the dashboard refetches that data over the normal REST API. It falls back to polling while the socket is down.
|
||||
## 9. In-memory rate limiting
|
||||
|
||||
**Rationale:** Events never carry data, so a socket can't see anything its owner couldn't fetch, and the REST API stays the single source of truth.
|
||||
|
||||
## 7. In-Memory Rate Limiting
|
||||
|
||||
**Decision:** Use NestJS ThrottlerModule with in-memory storage.
|
||||
|
||||
**Rationale:** Suitable for single-instance deployments. The abstraction allows swapping to Redis without controller changes.
|
||||
|
||||
## 8. Venue Isolation via JWT Claims
|
||||
|
||||
**Decision:** Venue ID is embedded in the JWT token. All service methods verify the requesting user/device's venue context rather than trusting URL parameters alone.
|
||||
|
||||
**Rationale:** Prevents cross-venue access even if a user manually modifies URL parameters.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user