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.
What is in place
Section titled “What is in place”- 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 demoagainst 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);
/syncis two queries regardless of how many pairs are open (a statement-count test guards it); the chart can put pgbouncer (CNPGPooler) in front of Postgres and the API supports it. - Abuse controls — Traefik rate limits per IP on
/authand on/me,/sync,/pairs; per-account login lockout, phone-start caps, and a publish quota (VOUCH_PUBLISH_PER_HOUR, 429 withRetry-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 plumbing —
GET /me/exportreturns everything stored about the caller;DELETE /mecascades; 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.mdholds the data inventory and the privacy notice;docs/legal-review.mdthe review findings. - Operability —
GET /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 thevouchCLI.just devbrings up the whole stack;just checkruns 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
kubectlgoes 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/persistentstack 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
clickhouseexporter. 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 lintand template, the collector config validation, andtofu validatefor 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).
Availability
Section titled “Availability”- 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) andpostgres.instances: 3. - Single AZ. Multi-AZ needs subnets and a second node set.
Security
Section titled “Security”- 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_changesprotects 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.
Privacy
Section titled “Privacy”- 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.
Product
Section titled “Product”- 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.