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 · The outbox
You will produce: Transactional outbox + relay (ADR-06); the crash-in-the-gap test
3.4 · JetStream and the first consumer
You will produce: Stream config; shared subject grammar; a durable pull consumer that dedupes
3.5 · Webhooks that survive the customer
You will produce: A dispatcher service: HMAC signing, a due-time retry schedule, dead letters
3.6 · When to stop trying
You will produce: Attempt records on an analytics stream, and auto-disable from two triggers
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 · 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.9 · The email nobody was sending
You will produce: The outbox pattern a third time, over a column chapter 3.6 already wrote — and Mailpit, because only a received message can prove an email carries no secret
3.10 · Quotas and what they cost
You will produce: Monthly quotas, spending caps, and degradation that rejects sends without touching history
3.11 · 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.12 · Milestone: the isolation gauntlet
You will produce: A cross-tenant suite whose target list derives itself from the running router, four attack shapes over 24 routes, a structural check that every table has a tenant path, the socket surface attacked from the protocol's own frame union, and three deliberate reintroductions — one of which stayed green and taught the suite's range
3.13 · The endpoints and the instruments
You will produce: The two public endpoints Part 3 needed and nobody had built, 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 the api repository layer's branch coverage answered with a number
3.14 · Milestone: errors that resolve, and an outsider
You will produce: Thirteen error codes with one registry and one URL rule, a docs_url that resolves against the published site, 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
3.15 · 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.16 · 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.17 · The sender a message never had
You will produce: Bot users carrying a description the database requires, a sender required on every message and enforced by the compiler rather than a test, an application credential that may speak as software and not as any person, refusals that reveal nothing about who exists, and a bot that is billed as an active user while being exempt from the ceiling that refuses sends
3.18 · 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 where the gateway's client hangs for ever, and a P1 clause measured as unmet and recorded rather than narrowed
3.19 · Presence, and who is allowed to see it
You will produce: A frame that has been in the protocol union since chapter 1.3 and had no producer, a second subject grammar that leaves 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 for ever, and a delivery scope with no filtering code in it at all
3.20 · The membership that changed under a live socket
You will produce: A clause unmet since chapter 2.6 and asserted as violated by a test since 3.18, closed with that test inverted and its 5,500 ms wait unchanged; a third subject grammar whose second shape addresses a principal rather than a channel, because an addition cannot ride the subject its own instance has not subscribed to; a ban that arrives as one change and leaves as one frame per channel, with the sentinel never reaching a client; and a periodic re-read standing in for a cursor that does not exist
3.21 · The frame nobody may send
You will produce: The first second inbound frame in twenty chapters, behind a named set whose size and membership 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; a two-second renewal interval that is a different quantity from FR-RTM-08's five-second expiry, at 2.5 renewals per window so one dropped publish does not flicker; and an honest verdict on a clause this platform cannot perform — no frame ends an indicator, so the timer is the receiving client's
3.22 · The sixth connection, and where the count lives
You will produce: FR-RTM-09 closed in both halves: a five-connection cap that no gateway instance can compute on its own, held as five slot keys claimed with `SET NX PX` and renewed with `SET IFEQ PX`; a sixth close code, because all five existing ones send a client to the wrong remedy and this is the only refusal in the set whose correct handling is not a retry; a refusal that completes the handshake in order to close it, because a browser cannot read the body of a failed upgrade; and a cap that fails open loudly, where the log line is the only thing that distinguishes `unenforced` from `under the limit`
3.23 · The words somebody wants back
You will produce: FR-MSG-07, FR-MSG-08 and FR-MSG-10 built, and the last two of FR-RTM-05's six event kinds given their first producers: a fifth subject grammar, `revision:{channel_id}`, carrying both mutations with the kind in the payload, because a tombstone is not a `Message` and an edit is one and would be indistinguishable from a creation; `message_edits` reproduced from SAD §6.1 as published, with what the composite key costs written down; two error codes rather than the generic 403, because no credential grants authorship and no permission change makes a message yours; a tenancy check taught that reachability is not adjacency, after it refused the new table in four milliseconds; and the one soft edge documented rather than closed — a message older than a client's cursor that changed during a disconnect produces no frame **and no sequence gap**, so the mechanism that repairs every other missed frame sees nothing to repair
3.24 · 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.
The road ahead
- Part 4The second data pathforthcoming
- Part 5Developer experienceforthcoming
- Part 6Shipping itforthcoming
- Part 7Running itforthcoming
- Part 8The retrospectiveforthcoming