3.2 KiB
Architecture
KoolTap is a modular monolith: one NestJS API, one Next.js dashboard, one PostgreSQL database, and a shared package that holds the contract between them and the bar terminals.
The server holds the venue
The venue's open tabs, stock and catalog live on the server. A terminal records what happens on it as events
(tab_opened, line_added, settled, stock_set, product_updated, …) in an outbox, and every sync:
- sends the outbox; the server stores each event once (by its id) and applies it to the venue,
- gets back the venue's complete state: the open tabs, the stock, the catalog (only when it changed) and the settlements since its cursor,
- replaces its own state with that and replays the events it still has to send on top.
Terminals apply the same rules the server does, so a terminal that is offline keeps working and lands in the same state once it syncs. The dashboard changes the same state directly (settling or deleting a tab, stock movements, products); terminals see that on their next sync. See terminal-protocol.md.
API (apps/api)
| Module | What it does |
|---|---|
ledger/ |
The rules, pure and unit tested: tab-state (tabs, lines, drinks, settling), stock-rules, product-fields (per-field last-write-wins), and venue-ledger, which loads a venue under a row lock, applies changes and writes back only what changed |
terminal/ |
Pairing, sync and picture uploads for terminals; device token auth; the minimum app version |
auth/ |
Dashboard sign-in with httpOnly cookies, refresh token rotation, GET /auth/me |
tabs/, customers/, stock/, reminders/, venues/ |
The dashboard API, one module per page |
admin/ |
The superadmin page: venues, terminals, users, resets, consistency checks, purges; daily housekeeping |
realtime/ |
Socket.IO: tells dashboards which data changed (topics only, never data) |
email/ |
Templates in templates/emails and Resend |
images/ |
Product pictures, processed with sharp and stored on disk by content hash |
Every request body and query is validated with a zod schema from @kooltap/shared; responses are typed with the
shared interfaces, so the dashboard and the API can't drift apart.
Data
- TabUnit is one drink ever rung up. It belongs to an open Tab until a Settlement takes it; a drink taken
off again keeps its
removedAtand moves with the settlement. Lines are the units sharing alineId. Sales reports are plain SQL over this table. - Product holds the catalog and the current stock; StockMovement is the dashboard's deliveries, breakage and counts.
- TabEvent is what terminals reported, as they sent it: the record of which events were applied.
- Customer is a regular: the next sync opens a tab for a customer without one, and their email is used for reminders. Tabs match customers by name.
- Everything belongs to a Venue and is deleted with it.
Dashboard (apps/dashboard)
Next.js App Router, rendered on the client. TanStack Query for data, refetched when the realtime socket says a topic
changed (polling only while the socket is down). Translations in src/locales/{en,nl}.json.