Platform
Architecture overview
Totlob has three TypeScript codebases: one Postgres-backed API and two React single-page apps. A family of versioned contract packages binds them together and defines every endpoint's path and schema.
System map
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ merchant-dashboard-v2│ │ totlob-pos │ │ storefronts / tools │
│ React 19 · Vite │ │ React 19 · Vite │ │ (outside these repos)│
│ Vercel │ │ Vercel │ │ │
└──────────┬───────────┘ └──────────┬───────────┘ └──────────┬───────────┘
│ @totlob/contracts-merchant│ @totlob/contracts-pos │ contracts-storefront
│ @totlob/contracts-accounting │ contracts-internal
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│ api-v2 ("cypher") Express 4 + ts-rest · TypeScript · Heroku (cypher-prod) │
│ /merchant/* /pos/v1/* /storefront/v1/* /internal/* /internal/ops/* │
│ 34 domain modules · PgTyped SQL · bigint money · per-shop ledger │
└───────┬───────────────┬───────────────┬───────────────┬──────────────────────┘
▼ ▼ ▼ ▼
PostgreSQL 16 Redis (cache) RabbitMQ (events) Cloudinary · Mailgun ·
(Stackhero) optional optional Google Maps · Algolia ·
Apple/Google IAP · FX feed
The three codebases
| Repo | Role | Stack | Hosting |
|---|---|---|---|
api-v2 (GitHub totlob/cypher) | All data and business rules. Serves five audiences: merchant, pos, storefront, internal, customer (empty). | Node ≥ 20, Express 4, ts-rest, zod 3, PgTyped, dbmate, vitest | Heroku cypher-prod; Postgres on Stackhero |
merchant-dashboard-v2 | Back office for owners and staff | React 19, Vite 6, React Router 7, TanStack Query 5 + Table 9, shadcn/ui, Tailwind 4, Recharts | Vercel (SPA rewrite) |
totlob-pos | Point-of-sale till | React 19, Vite 8, Zustand 5, TanStack Query 5, shadcn/ui, Tailwind 4 | Vercel (SPA rewrite) |
Cross-cutting design decisions
Contracts are the API. Every endpoint is declared in a published @totlob/contracts-* package (ts-rest + zod). The API lifts routes by reference into its routers, and the apps build typed clients from the same objects, so paths and schemas can't drift. See Shared contracts.
RPC over POST. Every route is POST /<audience>/<service>/v1/<kebab-name> with a JSON body. Shop-scoped routes carry shopId in the body. The middleware verifies it and handlers read verifiedShopId(req), never the raw body.
Passwordless auth with rotating refresh tokens. Email + 6-digit OTP. Short-lived HS256 JWT access tokens, plus opaque refresh tokens that rotate on every use and have reuse detection. Both apps serialise refreshes to avoid tripping reuse detection. Apps hand sessions to each other with 60-second single-use handoff codes. See Auth, roles & permissions.
Owner or staff, scoped three ways. A request resolves to a ShopAccess: either owner (everything), or staff with assignments that combine permission codes (what), branches (where) and catalogue category roots (which).
Money is never a float. All three codebases use fixed-point bigint at scale 6, matching Postgres numeric(20,6). The POS contract sends money as strings. The merchant contract uses numbers.
Catalogue prices are stored in USD and converted to the shop's currency on every read, using dated exchange rates (shop override > shared rate).
The server prices everything. The till mirrors the pricing maths for display only. Tax, discounts and totals are decided by the API at create-order, and every order write is idempotent on a client-generated key.
Module ownership. Each table belongs to exactly one API module. Other modules go through its service layer, which keeps per-namespace cache invalidation correct. See Structure & conventions.
Where things run
| Concern | Where |
|---|---|
| API deploy | Push to GitHub master triggers the Heroku deploy. release: runs dbmate migrations before new dynos take traffic. |
| App deploys | Vercel, per repo, from master. |
| Background work | worker dyno (npm run consumer) for ledger postings and search reindexing, or in-process with WEB_CONSUMES_QUEUE=true (never both). |
| Scheduled jobs | Heroku Scheduler: npm run sync-rates daily at 06:00 UTC (FX, in progress). |
| Logs | BetterStack (Logtail), request metadata only. |
master is production
Pushing to master in api-v2 or totlob-pos ships to real users, and API migrations ride along. See Deployment.