vault

02 — Architecture

docs/02-Architecture.mdupdated: 2026-08-09

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.
Mail 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 gateslib/auth/setup-gate.ts and lib/auth/kyc-gate.ts force password/MFA enrolment and KYC completion before sensitive surfaces.
  • Session revocationusers.session_version plus auth_session_revocation_tokens invalidate live JWT cookies; device recognition lives in auth_known_devices.
  • IntegratorsAuthorization: Bearer <token> (or x-api-key) → sha256 → api_keys row.
  • 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; impersonateAction refuses 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.execute pg-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'))

Drawbacks

Open Questions