vault

08 — Risks and Drawbacks

docs/08-Risks and Drawbacks.mdtype: risk-registerupdated: 2026-08-09

Risks and Drawbacks

A consolidated register written from the PM seat. Severity is reassessed each sprint.

Mission scope

  • ⚠️ Provider readiness gap (narrowed). Fireblocks, KYC (Sumsub), email and the rate oracle have live implementations. Elliptic, the acquirer, the chain provider and TRES are still mock-only and throw on live — so AML, sanctions and card acquiring cannot go live today. 05-External Providers
  • ⚠️ Single market launch unstated. KPI thresholds in 01-Overview are placeholders pending 09-Open Questions#Launch market.

Architecture

  • Background queue shipped. pg-boss runs 11 queues in a dedicated worker process; payouts, webhooks, mail, expiry sweeps and reconciliation are off the request lifecycle. The load-balancer-timeout risk on 1000-row payouts is closed.
  • runJob fire-and-forget removed — replaced by durable pg-boss jobs.
  • ⚠️ Single-region Postgres. No read replicas, no failover plan articulated. 09-Open Questions#Multi-region
  • ⚠️ Payout rows still execute sequentially inside the worker. Throughput, not durability, is now the constraint. 09-Open Questions#Payout perf
  • ⚠️ /v2 is an undocumented parallel UI. app/v2, components/v2, lib/v2 and lib/v2mocks implement a second front-end partly on mock data, with no PRD page, no migration plan and no stated relationship to the shipped surfaces. Divergence risk grows the longer both exist.

Auth

  • Admin escalation via env var removed. Admin access now comes from persisted users.role = 'admin', so envvar access alone cannot self-promote an account. Bootstrap/recovery requires a deliberate database role update.
  • JWT cookie revocation shipped. users.session_version plus auth_session_revocation_tokens invalidate live cookies; /reset/sessions/[token] gives users a self-service path. The old "valid until 30-day expiry" window is closed.
  • ⚠️ Second-factor secrets are app-stored. Password hashes (scrypt), TOTP secrets and recovery-code hashes all live in our own database. TOTP secrets are stored in a form the app can read — encrypting them at rest belongs with 09-Open Questions#Secrets at rest.

Domains

Ledger

  • ⚠️ Wallet history is unbounded read. Page size is 50 but filter+sort happen post-fetch — fine today, will need indexed pagination at scale.

Fees

  • ⚠️ Rate changes prospective only. Existing pending invoices keep their persisted platform_fee_minor.
  • Tier model retired. fee_tiers, users.tier, resolveUserFeeTier and resolveOpFeeTier are gone; rates are per-merchant columns on users plus global platform_fees knobs. The old "per-operation rates not plumbed everywhere" risk no longer applies.
  • ⚠️ Per-merchant rates have no defaults surface. Rates live on each user row, so a newly-created merchant's fees depend on whatever the insert path sets. There is no admin view of "all merchants on non-standard rates".
  • ⚠️ Public fee-schedule commitment is now harder to make honestly, since pricing is per-merchant rather than per-published-tier. 09-Open Questions#Fee schedule public commitment

Invoices

  • ⚠️ Per-invoice sub-vault is opened on every opened invoice. Costs one Fireblocks vault per invoice.
  • ⚠️ Rate-frozen invoices never re-quote — math may diverge from oracle if edited.

Payouts

  • ⚠️ AML cache is per-(address, network, currency), not per-merchant. Acceptable — Elliptic's API is per-address.
  • ⚠️ Bearer-token endpoint backed by api_keys, not a per-session /auth/token mint as in the spec. Revisit if integrators ask for short-lived tokens.
  • ⚠️ Funds preflight trims from the tail (highest row_num). The merchant has no UI control over which rows get bumped.
  • ⚠️ Mocked 1% Fireblocks failure is non-deterministic per call → tests for BATCH_PARTIAL are probabilistic.

Sell

Pay-to

  • ⚠️ Email confirmation rate-limit is per (slug, IP) — a determined attacker can rotate IPs.

Data model

  • ⚠️ audit_log.metadata schema is loose. Useful but hard to type-check downstream consumers.

Provider readiness

  • ✅ Live implementations landed for Fireblocks, KYC (Sumsub), email (Resend) and the rate oracle (CEX.IO).
  • ⚠️ Five interfaces remain mock-only and throw on live: chain, acquirer, risk, Elliptic, TRES. AML/sanctions and card acquiring are the critical-path blockers for a live launch.

Security

  • ⚠️ Webhook secrets are not yet KMS-encrypted. Encrypt merchant callback secrets at rest before GA.
  • ⚠️ TOTP secrets are stored plaintext in users.totp_secret. A database read discloses every user's second factor. Pair with the webhook-secret work under 09-Open Questions#Secrets at rest.
  • Rate-limit storage moved to Postgres (rate_limits). It survives deploys and is shared across instances — the in-process-map risk is closed.

Admin

  • ⚠️ No undo on fee-rate change or KYC override.
  • ⚠️ Partial bulk export. /admin/logs and /api/invoices/export export; /admin/transactions and /admin/users still require copy/paste.

Open Questions