vault
02 — Architecture
Architecture
How the shipped system is put together today — stack, layering, auth, concurrency and background work. For the planned AWS rebuild, see 11-Target-Architecture instead.
Stack
| Layer | Choice | Why |
|---|---|---|
| App framework | Next.js 16 App Router (server components + server actions) | Single deployable for marketing, web app, public APIs. |
| ORM | drizzle-orm | Strong types, raw-SQL escape hatch, migrations as plain .sql. |
| DB | Postgres 14+ | ACID, JSONB metadata, partial indexes used by invoice mirror dedupe. |
| Auth | NextAuth v5 (JWT cookie) for web, with app-local credentials: password (node:crypto scrypt, N=65536), TOTP, passkeys (WebAuthn), magic link, recovery codes; api_keys (sha256) for integrators. |
One identity model for both surfaces. |
| Client data | oRPC v1 (@orpc/server + @orpc/client + @orpc/tanstack-query) |
Type-safe queries from React without REST shape duplication; standard HTTP semantics + native OpenAPI integration. |
Pluggable provider; mock writes mock_emails, live delivery uses Resend. |
EMAIL_PROVIDER=mock or resend swaps app notifications without changing call sites (05-External Providers#Email). |
|
| Custody | Pluggable FireblocksProvider, ChainProvider. Fireblocks has a live impl (LiveFireblocks); ChainProvider is mock-only. |
Live cutover swaps the impl, not the call sites. |
| AML | Pluggable EllipticProvider; cache-aside with 24h freshness. Mock only. |
Pay-per-call API; cache mandatory. |
| Background work | pg-boss on the same Postgres (worker/worker.ts). |
Durable retries without a second datastore. |
Layering
app/ Routes (App Router)
(auth)/ Login, register, password / TOTP / passkey / recovery setup, KYC gate
(public)/ Public surfaces (pay-to, public invoice, order checkout, /[slug] CMS pages)
my/ Authenticated retail + merchant flows (wallet, send, receive, buy, sell,
convert, payees, invoices, payouts, orders, notifications, api-keys, profile)
api-orders/ Merchant API-order console + config
api-payouts/ Merchant payouts-API console
admin/ Admin console
agent/, merchant/, user/ Role landing surfaces
v2/ In-progress UI rewrite (see below)
prd/ Public PRD vault — `docs/` rendered as HTML at build time
api/v1/ Integrator REST API
api/webhooks/ Inbound provider callbacks (fireblocks, kyc, mercuryo)
api/rpc/ oRPC transport
components/ Client components (only `use client` where needed)
domain/ Shared product UI (FilterGrid, tables, forms)
v2/ Components for the /v2 rewrite
lib/
db/schema/ drizzle schema — a DIRECTORY of per-domain files, not one schema.ts
db/migrations/ hand-written .sql + meta/_journal.json
auth/, fees/, aml/ Domain helpers
mail/ Templates + per-event notifiers
providers/ External-system abstraction (mock + live)
orpc/routers/ Per-domain oRPC routers (admin/*, my/*, wallet, buy, convert, aml)
payouts/ Bulk-payout helpers (auth, engine, execute, holds, limits, CSV, address format)
reconciliation/ Balance reconciliation runs + outbound halts
notifications/ In-app notification dispatch + SSE listener
observability/ Logging, redaction, heartbeats, outgoing-API logging
rate-limit/ DB-backed windows + login lockout
worker/ pg-boss worker process
jobs/ Job handlers + queue definitions
/v2/* is an unreleased UI rewrite (app/v2, components/v2, lib/v2, lib/v2mocks) that runs partly on mock data. It is not covered by the domain statuses in 03-Domains and has no PRD page yet.
Identity & auth flow
- Web users → app-local NextAuth credentials providers (password, TOTP, passkey, magic link, recovery code) → NextAuth session → JWT cookie. All credential material is app-owned.
- Step-up / setup gates —
lib/auth/setup-gate.tsandlib/auth/kyc-gate.tsforce password/MFA enrolment and KYC completion before sensitive surfaces. - Session revocation —
users.session_versionplusauth_session_revocation_tokensinvalidate live JWT cookies; device recognition lives inauth_known_devices. - Integrators →
Authorization: Bearer <token>(orx-api-key) → sha256 →api_keysrow. - Admin role:
users.role = 'admin'. Admin grants are persisted in Postgres; env-based admin escalation is not supported. - Block enforcement:
users.blocked_at IS NOT NULL⇒ login refused;impersonateActionrefuses to mint a session for blocked targets.
Concurrency model
- Financial mutations open a Postgres transaction and acquire a per-(account, asset) advisory lock via
pg_advisory_xact_lock(lib/wallet#lockAccountAsset). - Convert acquires both source and destination locks in sorted order to avoid the A→B / B→A deadlock pattern.
- Payouts process one row at a time; each row is its own atomic unit (debit + row update + audit_log) so a mid-batch failure leaves earlier successes durable.
- Payout execution is dispatched to the
payout.executepg-boss queue rather than run inside the request, so a 1000-row batch no longer risks a load-balancer timeout. - Batch funds are reserved up-front via a hold (
lib/payouts/hold.ts); per-row refunds and fee adjustments settle against that hold and the remainder is released at the end.
Background queues
worker/worker.ts consumes eleven pg-boss queues:
| Queue | Purpose |
|---|---|
payout.execute |
Bulk-payout batch execution |
send.submitted / buy.submitted |
Outbound send + card-buy orchestration |
webhook.deliver |
Merchant callback delivery with backoff |
provider.webhook.process |
Durable inbox for provider callbacks (provider_webhook_events) |
dev-only.provider.webhook.dispatch |
Local emulation that re-enters the real ingestion path |
mail.send |
Outbound email |
order.expire.verify + order.expiry.tick |
Intent TTL sweep |
invoice.reminders.tick |
Due-date reminders |
fireblocks.poll.pending |
Polls pending custody transactions |
reconcile.balances |
Ledger-vs-custody reconciliation runs |
Idempotency keys
| Operation | Key |
|---|---|
| Send | send:{transactionId} |
| Buy | buy:{transactionId} |
| Sell | sell:{transactionId} |
| Convert leg | `convert:{transactionId}:{from |
| Deposit | deposit:{txHash} |
| Invoice settlement | invoice:settle:{invoiceId} |
| Payout row | payout:row:{rowId} |
| Batch dedup | payout_batches partial unique index (user_id, merchant_batch_id) |
| Invoice mirror | partial unique index transactions ((metadata->>'txHash')) |