ADR-002: Location-of-authority state split
Date: 2025-01-15 (Phase 1) Status: Accepted Supersedes: — Superseded by: —
Context
An IRC server manages several kinds of state: connection records (nick, caps,
away status), channel rosters (who is in #foo, with what prefix), channel
metadata (topic, modes, ban lists), and the nick registry (nick → connection
map). In a single-process ircd all of this lives in one address space, so
read-modify-write races are handled with locks or a single event loop.
ServerlessIRCd runs on stateless-per-request compute (Lambda) or per-entity-isolated compute (Durable Objects). There is no global lock. If two connections race to JOIN the same channel, or two clients race to claim the same nick, the outcome must still be correct. Traditional locking is impossible across instances.
Decision
Each piece of state has exactly one authority — the single entity that owns the canonical copy and serializes all mutations to it. Reducers are routed to the authority that owns the state they mutate:
| Command family | Authority | Primary state |
|---|---|---|
| Registration (NICK/USER/PASS) | Connection entity | ConnectionState |
| PING/PONG, QUIT, AWAY, user MODE | Connection entity | ConnectionState |
| JOIN/PART/channel MODE/TOPIC/KICK/INVITE | Channel entity | ChannelState + roster |
| PRIVMSG/NOTICE to channel | Channel entity | Fanout (reads roster) |
| PRIVMSG/NOTICE to user | Sender Connection | Nick → Connection route |
| Nick reservation | Registry entity | Nick → Connection map |
Cross-authority coordination is done via effects, not direct mutation.
When a connection accepts a JOIN, the connection-authority reducer emits an
ApplyChannelDelta effect that the channel authority consumes. The channel
authority is the sole writer of its roster; the connection never touches it
directly.
Platform mapping:
- Cloudflare: each authority is a Durable Object (
ConnectionDO,ChannelDO,RegistryDO). DO single-threaded serial execution gives the uniqueness invariant for free — two concurrent requests to the same DO are queued, not racing. - AWS: each authority is a DynamoDB table with conditional writes
(
ConditionExpression) orTransactWriteItemsfor multi-row atomicity (e.g. JOIN spansConnectionsandChannelMembers).
Consequences
Positive:
- Races are structurally impossible, not defended against. Two clients
claiming the same nick both route to the same
RegistryDOshard (or the sameNicksrow with a conditional PutItem) — exactly one wins. - The reducer signature stays uniform (
Reducer<S>) regardless of which authority it runs in. The actor layer routes; the reducer does not know or care whether it is running in a DO or a Lambda. - Each authority can be tested in isolation. The channel reducer tests
arrange a
ChannelStateand assert effects; they never need a live connection registry.
Negative:
- Cross-authority coordination requires a round-trip (DO
stub()call on CF; extra DynamoDB write on AWS). A JOIN that updates both the connection's joined-list and the channel roster is two writes, not one. - The actor layer must pre-fetch authoritative state before invoking a
reducer (e.g. WHOIS needs the target connection's state from a different
authority). This adds a
prefetchChannelStatestep in the actor pipeline. - Listing all channels (LIST) or resolving every member of a channel (WHO) requires aggregating across authorities — there is no global view. The CF adapter needs a channel registry (TICKET-073); the AWS adapter does a DynamoDB scan.
References
- PLAN §2.1 — location-of-authority table
- PLAN §4 — state model & authoritative ownership
- ADR-001 — pure reducers (how effects flow between authorities)
packages/irc-server/src/actor.ts—route()switch (authority routing)