vault

07 — Non-Functional Requirements

docs/07-Non-Functional.mdupdated: 2026-08-09

Non-Functional Requirements

The cross-cutting properties: security, observability, reliability, compliance posture, permissions and disaster recovery. Each section states what is in place today and what is still open.

Security

  • Secrets: all credentials in env (AUTH_SECRET, DATABASE_URL, etc). API keys stored as sha256 hash only.
  • Address validation: per-network regex enforced before any AML lookup (server + client).
  • Webhooks signed with webhook_secret; consumers must verify.
  • Rate limiting is DB-backed (rate_limits table, lib/rate-limit/window.ts + lockout.ts) — it survives deploys and is shared across instances. Applied to REST (lib/api/rest-limits.ts, keyed per API key), login/TOTP lockout, and pay-to email submission.
  • App-local interactive auth: scrypt password hashes, TOTP secrets, WebAuthn passkeys and recovery-code hashes are stored by the app. users.session_version + auth_session_revocation_tokens allow session revocation; auth_known_devices drives new-device detection.
  • API keys carry an expires_at in addition to revoked_at.
  • audit_log is append-only at the DB level (0012_audit_log_append_only).
  • Log redaction is centralised in lib/observability/redact.ts (auth headers, session tokens, secrets, challenge/passkey values, PII) and applied to every sink.
  • ⚠️ Still open: webhook_secret and high-risk PII are not encrypted at rest (09-Open Questions#Secrets at rest).
  • See 08-Risks and Drawbacks#Security.

Observability

  • audit_log is the spine — every state-changing event lands there with structured metadata.
  • Mocked email outbound visible at /admin/emails.
  • OpenTelemetry is wired. instrumentation.ts boots @opentelemetry/sdk-node with the OTLP HTTP trace exporter (Node runtime only) when the Better Stack env vars are set.
  • Structured logging via Pino with a @logtail/pino transport to Better Stack; falls back to stdout/CloudWatch when unconfigured.
  • Browser telemetry (Web Vitals + global errors) and server error capture via onRequestError.
  • Health checks: /api/health (shallow, for the ALB) and /api/health/deep (probes the DB, returns 503 on dependency failure).
  • Dead-man switches: worker + web heartbeats.
  • Outgoing-API logging (lib/observability/outgoing-api-logging.ts) instruments provider calls.
  • ❌ Still no metrics export (Prometheus /metrics planned).
  • Full rollout detail: observability/better-stack.

Reliability

  • pg-boss is live. worker/worker.ts runs a dedicated worker process over 11 queues; payout execution, webhook delivery, mail, order expiry, invoice reminders, Fireblocks polling and balance reconciliation are all off the request lifecycle. The old fire-and-forget runJob helper is gone.
  • Durable inbound provider callbacksprovider_webhook_events inbox + provider.webhook.process queue, so a crash replays rather than drops.
  • Outbound circuit breaker — reconciliation drift beyond the hard threshold opens an outbound_halts row that pauses sends and payouts for that asset.
  • ⚠️ Worker topology, retry policy and cron ownership are still being tuned — see 09-Open Questions#Background queue.

Compliance

  • Block flag on user prevents login + impersonation.
  • KYC override audited.
  • AML score recorded per row in payouts.
  • FATF Travel Rule (real-time originator + beneficiary identity per transfer): not implemented. EU TFR threshold is zero — applies to any amount once we touch EU traffic. Vendor / protocol open at 09-Open Questions#Travel Rule integration.
  • DAC8 (annual tax reporting per customer per asset): not implemented. Applies from 1 January 2026; first cycle 2027. Build plan at 09-Open Questions#DAC8 build.
  • ⚠️ Sanctions screening (OFAC / EU consolidated / UK HMT): delivered via Elliptic in the target architecture (Elliptic combines wallet-AML risk and sanctions in one product). MVP only mocks AML risk; live cutover lights up sanctions automatically. Required for Travel Rule compliance.
  • See 15-Compliance for the full brief and engineering surface.
  • ❌ Tax reporting: see 09-Open Questions#Tax & regulatory.

Permission matrix

Account Scope Privileged actions allowed
User Own data — own balance, history, product actions None at platform level
Merchant Merchant-bounded — merchant data, API, subaccount delegation within boundary Subaccount grants (03-Domains#Subaccounts planned), API key revoke (own keys), webhook config
Admin Internal role-based — least privilege; not all admins have all sections Full list at 14-Operations#Privileged actions

Admin role granularity (which admin gets which section) is itself an open question — see 09-Open Questions#Admin role split.

Compliance posture

The architecture is built to satisfy regulated-operations expectations rather than to retrofit them. Detailed framing lives in 14-Operations#Compliance themes; below is the at-a-glance picture.

DORA-relevant

  • Environment separation (12-Environments).
  • Production-like testing posture (Test mirrors Prod).
  • Logging + monitoring (audit_log, CloudTrail planned).
  • DB DR for Test + Prod (table below).
  • Provider dependency visibility (05-External Providers).
  • Controlled admin access (AWS IAM Identity Center SSO + dedicated AWS WAF ruleset, planned).
  • Documented security boundaries (13-Network-Model).

MiCA-relevant

CASP licensing direction tracked at 09-Open Questions#Tax & regulatory.

Disaster recovery (target)

Scope Dev Test Prod
DB backup Best-effort Required Required
DB DR to secondary region None Required Required
Application failover Single AZ Multi-AZ Multi-AZ
RPO target n/a < 1h < 15min
RTO target n/a < 4h < 1h

DR is DB-first — the rest of the application can be redeployed from artifacts. See 12-Environments#DR strategy.

Performance budget

Surface P95 target Today
/my/wallet initial render < 400 ms within budget
/my/payouts/new validate (30 rows) < 1.5 s within budget
/my/payouts/new validate (1000 rows) < 8 s untested — see 09-Open Questions#Payout perf
POST /api/v1/payouts/:id/execute < 1 s to acknowledge now enqueues onto payout.execute; the request no longer waits for row execution
Payout batch drain (1000 rows) < 60 s sequential per-row in the worker; mocked latency 60 ms; ≈ 60 s expected

The old "1000-row execute times out behind the load balancer" risk is closed by the queue cutover. Batch throughput is still sequential — parallelism is open at 09-Open Questions#Payout perf.