Skip to content

The road to production

An honest account of what the system provides and what it does not, most load-bearing first. It describes the system as built; how to check a deployment is in Deploy to the cluster and the checks at the end of scripts/deploy_cluster.sh.

  • Matching protocol — DH-PSI on ristretto255; the server is crypto-blind. Two-user matches are exercised in the server suite, in the Playwright browser tests, and by vouch sim demo against any running deployment. Published sets are padded to 64-element buckets so the server cannot count a user’s contacts.
  • Identity — username/password (argon2id, lockout) and Google OIDC with PKCE; revocable sessions with rotating refresh tokens; phone verification by SMS with a DLEQ proof binding the published own-token to the verified number.
  • Data model — every primary key is a PostgreSQL 18 uuidv7(); matches are keyed by their pair. A test applies the Alembic chain to a scratch database and fails on any drift from the models.
  • Scale path — pairing on a user’s write is one query against every compatible user (O(n)); the whole-userbase sweep runs in the background in exactly one replica (Postgres advisory lock); /sync is two queries regardless of how many pairs are open (a statement-count test guards it); the chart can put pgbouncer (CNPG Pooler) in front of Postgres and the API supports it.
  • Abuse controls — Traefik rate limits per IP on /auth and on /me, /sync, /pairs; per-account login lockout, phone-start caps, and a publish quota (VOUCH_PUBLISH_PER_HOUR, 429 with Retry-After).
  • Moderation — users report the other side of a match; admins see the queue, ban (sessions revoked, every call answers 403 account_banned), unban, delete, resolve.
  • Data-subject rights plumbingGET /me/export returns everything stored about the caller; DELETE /me cascades; retention is enforced by the background loop (phone challenges 10 min, rate ledger 1 h, revoked sessions 30 d) and by the backup and telemetry TTLs. docs/privacy.md holds the data inventory and the privacy notice; docs/legal-review.md the review findings.
  • OperabilityGET /admin/overview (counts, matcher status, migration revision, redacted settings), user/pair/report views, a dev SMS outbox, all exposed in the client’s admin area and the vouch CLI. just dev brings up the whole stack; just check runs everything CI runs.
  • Server — FastAPI + CloudNativePG Postgres 18; migrations run once per release as a Helm hook Job (no per-pod race); API and web get PodDisruptionBudgets and anti-affinity when scaled above one replica.
  • Client — React SPA served by nginx; crypto in-browser (libsodium), bit-compatible with the server through shared vectors; key export/import for device moves.
  • Infra — k3s on two Graviton nodes, fck-nat egress, Traefik with Let’s Encrypt, ECR (immutable tags, images deployed by digest and signed with cosign keyless), all in OpenTofu. The k3s API is not exposed; every kubectl goes through an SSM tunnel.
  • Durability — PersistentVolumes on EBS gp3 through the AWS EBS CSI driver; CNPG continuous WAL archiving and a daily base backup to S3 with 30-day retention; the bucket lives in the separate infra/persistent stack that teardown never touches; restore procedure in Restore from backup.
  • Secrets — JWT secret, phone pepper, admin bootstrap token, Google client secret and HyperDX credentials live in AWS Secrets Manager (same persistent stack, generated once, never rotated by automation) and reach the cluster through External Secrets Operator using the node role. No secret passes through a laptop or a CI log.
  • Telemetry — the API, Traefik and HyperDX’s own app export OTLP to a collector we run (one contrib daemonset that also tails node logs), which writes straight into HyperDX’s ClickHouse with the clickhouse exporter. The collector config is validated with the real binary in CI.
  • Alerting — CloudWatch alarms on both nodes’ status checks and a Route53 HTTPS health check on api.<domain>/healthz, all notifying an email subscription.
  • CI — every push and pull request runs ruff, the server suite against Postgres 18, oxlint, tsc, vitest, the client build, Playwright browser tests (register → verify → publish → match across two browser contexts, admin ban/unban, outbox) against a throwaway Postgres, helm lint and template, the collector config validation, and tofu validate for all three stacks. Deploy and Teardown are manual actions.
  • Patching — nodes run unattended-upgrades (security pocket, reboot window 04:00); k3s installs from the stable channel.

Two Graviton nodes, one NAT instance, EBS, two public IPv4 addresses: about €52 a month. Small additions: Secrets Manager ($0.40 × 3), a Route53 HTTPS health check ($1.50), S3 for backups (cents at this data volume), EBS volumes for Postgres/ClickHouse/Mongo (€1–2).

  • Single node per role, single Postgres instance. The chart and tofu expose the knobs (api.replicas, web.replicas, postgres.instances, postgres.pooler.enabled, PDBs, anti-affinity, leader-locked matcher), but the defaults stay at one of each to hold the €50 budget. Going HA means a second apps node (+€25) and postgres.instances: 3.
  • Single AZ. Multi-AZ needs subnets and a second node set.
  • Secrets at rest in etcd are plain k8s Secrets once ESO has synced them (k3s encrypts etcd at rest only if configured). The source of truth is Secrets Manager.
  • The phone pepper cannot be rotated without every user re-verifying. Accepted: it is generated once and ignore_changes protects it.
  • Phone verification trade-offs (accepted): H(e164) is on disk for ≤10 min while a code is pending; AWS SNS sees the number in transit.
  • ClickHouse credentials for the gateway are the HyperDX chart defaults (cluster-internal only). Move them to Secrets Manager when HyperDX matters beyond debugging.
  • Key rotation is manual (regenerate in the account tab, then re-verify and republish). Multi-device is “export the key, import it on the other device”; there is no sync.
  • Account recovery without the key or a second login method is impossible by design; linking Google is the recommended safeguard.
  • Legal: the engineering changes required by docs/legal-review.md; that document tracks which of them the product implements.
  • A real mobile client. The SPA is a developer console; the consumer app is a separate build.
  • Moderation depth: a report/ban loop exists, but no appeals, no automated signals, no handling of false reports.