Architecture¶
Shape¶
- Web-on-LAN: one server box in the shop runs the API + Postgres; tills are browsers on the local network. Remote stations (a mobile cart) reach the same server over Tailscale.
- Device bridge: the only OS-coupled code. A small Node service on each till owns the ESC/POS receipt printer, the cash-drawer kick, and the USB-serial scale, exposed as a tiny localhost HTTP API the browser calls. Everything else is OS-agnostic (Windows today, Linux whenever).
- Payments: Stripe Terminal smart readers. DECIDED (Tai, 2026-07-06): counter stations get the BBPOS WisePOS E + Ethernet dock. Criteria: wired (a counter terminal must not depend on WiFi — the dock provides ethernet + continuous power) and ORDERABLE: standard Stripe accounts can buy the WisePOS E directly; the Verifone P630 from the original shortlist is restricted to invited/enterprise integrations. Canada + Interac confirmed, and Stripe's recommended integration for this reader is server-driven — exactly ours. (Full comparison: second-brain → Roasterie-POS → payment-stripe-terminal-findings.md.) Readers are driven server-side via Stripe's API; No card data in this system, ever — only PaymentIntent/charge references, brand, last-4. Voids refund against the original charge.
- Reporting is a read layer (views) over the transactional schema, added after the POS itself works. Never the other way around.
Build order¶
- Sale screen on mock data (current) — get the till flow right visually first.
- Tender screen (cash, split, gift card; Stripe stubbed) + receipt preview.
apps/server: Postgres schema as migrations + API; the web app switches from mock catalog to the real one. Domain types graduate topackages/shared.- Device bridge spike on real hardware (printer, drawer, scale) — the riskiest integration, de-risked early once a stack exists to host it.
- Stripe Terminal integration; then AR/wholesale, gift cards, drawer sessions, migration of 13 years of history from the legacy system.
Schema¶
The full target schema (8 domains: customers, catalog, sales, AR, gift cards, drawer/shifts,
employees, settings) was designed domain-by-domain against a restored backup of the legacy
system, with per-domain migration mappings. It lives in the project planning vault and becomes
apps/server SQL migrations in step 3.
Key modeling decisions:
- Money: integers (cents) in code,
numeric(12,2)in Postgres. - Ledgers, not balances: AR, gift cards, loyalty, and stock are append-only ledgers; balances are always derived sums, never hand-edited numbers.
- History-preserving: products and sales are never overwritten or recycled; prices are snapshotted on the sale line.
- Modifiers are products flagged
is_modifierthat can only be rung attached to the parent product currently being entered (sale_line_modifier) — still individually priced and reportable, never a standalone line. - Constraint-driven behavior lives on product + department attributes (
quantity_capture: unit/weigh/keyed;requires_customer), not UI modes. Departments carry defaults that pre-fill the new-item wizard; the product's own attributes are authoritative at runtime. - Stock is a per-item opt-in capability (
track_stock+ a movement ledger), not a burden. - Tax is per-product via
tax_class→ effective-datedtax_raterows (multi-rate ready). - Voids, not returns: a void flips the sale's status, posts tender-aware reversals (card → refund on the original charge, gift card → restore that card, on-account → reverse the AR charge), and logs a loss-prevention exception. Nothing is deleted.