Building Relay
Build a real-time chat platform company from an empty directory — specs, code, deployment, and monitoring included.
Part 0 — The idea and the paper
0.1 · From app to infrastructure — finding the real product
You will produce: A positioning statement; a non-goals list
0.2 · Four people who will judge us
You will produce: A persona set including the invisible end user
0.3 · Journeys — where products die
You will produce: Journey maps; the ★ moments
0.4 · Requirements you can test
You will produce: An SRS slice with IDs, priorities, verification methods
0.5 · Deciding out loud — the SAD and the ADR habit
You will produce: A drivers table; two ADRs written from scratch
Part 1 — Foundations
1.1 · The monorepo and the toolchain
You will produce: A running pnpm workspace — TypeScript, lint, and a passing test suite
1.2 · One command, whole world
You will produce: A one-command local infrastructure — four stores, healthchecked and verified
1.3 · The protocol package
You will produce: The shared wire contract — frame types, error codes, and schemas that reject bad input
1.4 · Walking skeleton
You will produce: Two running skeleton services — health-checked, request-ID'd, logging structured JSON
Part 2 — The core loop
2.1 · Schema with a spine
You will produce: A migrated schema and a tenant-scoped repository layer — cross-tenant leaks made inexpressible
2.2 · The write path
You will produce: POST message: channel row lock, sequence assignment (ADR-03)
2.3 · Send it twice
You will produce: Idempotency keys, partial unique index (DR-03)
2.4 · History that pages
You will produce: Cursor pagination on (channel_id, seq)
2.5 · The socket
You will produce: Gateway: WS termination, JWT verify, connection registry
2.6 · Two servers, one conversation
You will produce: Redis fan-out (ADR-07); the lossy-fabric argument
2.7 · The tunnel
You will produce: Resume protocol: cursors, backfill, subscribe-before-backfill buffer
2.8 · Milestone: the Tuan test
You will produce: An integration suite scripting journey 4 end-to-end — the SRS Phase 1 exit criterion
Part 3 — Becoming a platform
3.1 · Tenants all the way down
You will produce: Orgs, apps, environments; OAuth signup; the auto-created dev environment
3.2 · Keys and tokens — two credentials, one mistake
You will produce: API keys (prefix, hash, rotation); user JWTs; the dev-token endpoint
3.3 · Errors that resolve
You will produce: An error registry that declares every code the platform can send, one place that builds the docs_url, and a typed thrower so a mistyped code stops compiling
3.4 · The isolation harness
You will produce: A cross-tenant suite whose target list derives itself from the running router, three attack shapes that compare a pair of responses rather than a status code, a structural check that every table has a path back to one tenant, and the socket attacked from the protocol's own frame union
3.5 · The outbox
You will produce: Transactional outbox + relay (ADR-06); the crash-in-the-gap test
3.6 · JetStream and the first consumer
You will produce: Stream config; shared subject grammar; a durable pull consumer that dedupes
3.7 · Commit and publish are two instants
You will produce: The resume duplicate closed: a high-water mark that outlives the buffer
3.8 · The endpoints and the instruments
You will produce: The two public endpoints Part 3 needed, idempotency enforced by a unique index rather than in memory, every validation error naming its field for the first time, the global-operation guard watching nine tables instead of five, and repository branch coverage answered with a number
3.9 · The channel a customer controls
You will produce: A private channel type that decides something on all four of its doors, member removal and roles, archiving that refuses a send without announcing the channel exists, and a gauntlet that attacks your own tenant
3.10 · What a user sees
You will produce: Channel listing with cursor pagination and activity ordering, unread counts from the sequence the write path already maintains, user profiles created implicitly on first authentication, a deleted user whose messages survive, and a ban enforced at the door and on the send path
3.11 · The sender a message never had
You will produce: Bot users carrying a description the database requires, a sender on every message enforced by the compiler rather than a test, a credential that speaks as software and never as a person, refusals that reveal nothing about who exists, and a bot billed as active yet exempt from the send ceiling
3.12 · The message that never arrived
You will produce: A message sent over REST that reaches a live socket, an ordering that splits by transport because a request handler's response IS its acknowledgement, a publisher that survives a dead broker in 2 ms, and a P1 clause measured as unmet and recorded rather than narrowed
3.13 · Presence, and who is allowed to see it
You will produce: A frame in the protocol union since chapter 1.3 that finally has a producer, a second subject grammar leaving the message hot path byte-identical, a key whose existence is the state and whose SET … NX is the election, a grace period whose first fix stranded users online, and no filtering code
3.14 · The membership that changed under a live socket
You will produce: A clause unmet since chapter 2.6, closed by inverting the test that caught it; a third subject grammar addressing a principal rather than a channel, because an addition cannot ride a subject its instance never subscribed to; and a ban that arrives as one change and leaves as one frame per channel
3.15 · The frame nobody may send
You will produce: The first second inbound frame in twenty chapters, behind a named set a test pins; a fourth subject grammar taken rather than avoided, after re-deriving ADR-19's count and finding seven typed points where the record says three; and an honest verdict on a clause this platform cannot perform
3.16 · The sixth connection, and where the count lives
You will produce: FR-RTM-09 closed in both halves: a five-connection cap no gateway instance can compute alone, held as five slot keys claimed with `SET NX PX`; a sixth close code, the only refusal in the set whose correct handling is not a retry; and a cap that fails open loudly, the log line its only evidence
3.17 · The words somebody wants back
You will produce: FR-MSG-07, FR-MSG-08 and FR-MSG-10 built, and FR-RTM-05's last two event kinds given their first producers: a fifth subject grammar carrying both mutations with the kind in the payload, because a tombstone is not a `Message` and an edit is; and two error codes rather than the generic 403
3.18 · The message that is not only text
You will produce: FR-MSG-11's external-URL half built: a message carries attachments, bounded at ten and 2,048 characters, refused unless the scheme is http or https. The media_id half is deferred to §4.14 and refused by name, with a code of its own.
3.19 · Webhooks that survive the customer
You will produce: A dispatcher service: HMAC signing, a due-time retry schedule, dead letters
3.20 · When to stop trying
You will produce: Attempt records on an analytics stream, and auto-disable from two triggers
3.21 · The email nobody was sending
You will produce: The outbox pattern a third time, over a column the "When to stop trying" chapter already wrote — and Mailpit, because only a received message can prove an email carries no secret
3.22 · Limits you can see coming
You will produce: Per-environment request counters, the headers on every response, and two limiters that fail in opposite directions
3.23 · Quotas and what they cost
You will produce: Monthly quotas, spending caps, and degradation that rejects sends without touching history
3.24 · Counting a connection
You will produce: Connection-minutes metered from a service that owns no tables, a crash that under-bills by a bounded amount rather than over-billing for ever, and close code 4008 emitted for the first time since chapter 1.3 declared it
3.25 · Milestone: the isolation gauntlet
You will produce: Three deliberate reintroductions — one of which stayed green and taught the suite's range — and tests for the instruments that had never produced output
3.26 · Milestone: an outsider integrates
You will produce: A sealed integration package mechanically unable to import workspace code, and a verdict on the SRS Phase 2 exit criterion with what was measured and what was assumed
Part 4 — Everywhere the data went
4.1 · The question the counters can't answer
You will produce: A million-message corpus, the analytical query written against Postgres for the first time, and four numbers showing the index that should fix it costs 49% more storage and buys noise
4.2 · The store that was never listening
You will produce: A ClickHouse a query can reach, an analytical schema with a ledger of its own, and the same question answered in 13.22 ms against Postgres's 585.9 — with one day out of ninety-one that can never reconcile
4.3 · The consumer that was promised
You will produce: The ingester for a stream that has been filling since chapter 3.20 with nothing reading it — deduplicating on the record's own key, because the template built to prevent exactly this problem writes to the database this path may not touch
The road ahead
- Part 5Developer experienceforthcoming
- Part 6Shipping itforthcoming
- Part 7Running itforthcoming
- Part 8The retrospectiveforthcoming