ServerlessIRCd Docs

Docs home

ServerlessIRCd — Extensions Plan (post-v1)

Plan for the IRC features that v1 deliberately left out: the remaining IRCv3 client-server extensions and IRC services (NickServ / ChanServ / OperServ / HostServ / MemoServ). Each section is scoped, mapped onto the existing hexagonal architecture, and proposed as tickets for tickets.md / progress.md once ratified.

Status: planning. Nothing here has landed. Ticket numbers below start at TICKET-110 (next free id per todo.md). When this plan is ratified the tickets get appended to tickets.md / progress.md under two new phase headings (Phase 12 — IRCv3 extensions, Phase 13 — IRC services) and the corresponding sections here are superseded.


1. Goals & non-goals

Goals

Non-goals (this plan)


2. Current cap set (baseline)

packages/irc-core/src/caps/capabilities.ts:28 advertises:

account-tag, away-notify, batch, chghost, draft/chathistory,
echo-message, extended-join, invite-notify, message-tags,
multi-prefix, safelist, sasl=PLAIN[,EXTERNAL], server-time

What follows fills in the missing pieces, grouped by spec maturity.


3. Missing IRCv3 extensions

3.1 Stable spec extensions (high-value, low-risk)

Cap Spec What it adds Effort Depends on
account-notify ircv3.net/specs/extensions/account-notify-3.2 Push ACCOUNT messages to peers in shared channels on login/logout (complements account-tag, extended-join) S (2 pts) SASL AccountStore wiring (TICKET-071 ✅)
msgid ircv3.net/specs/extensions/message-ids Stable per-message id as a msgid tag on every PRIVMSG/NOTICE to cap-enabled peers (live fanout), sharing the single IdFactory.nonce() value also recorded for chathistory replay; needed for draft/read-marker, replies, dedup S (2 pts) message-tags ✅, IdFactory ✅
standard-replies ircv3.net/specs/extensions/standard-replies-3.0 Replace a class of bare numerics with FAIL/WARN/NOTE for cap-enabled clients; better client UX M (3 pts) none
monitor ircv3.net/specs/extensions/monitor-3.2 MONITOR command, modern replacement for ISON; per-connection watchlist + 730/731/732/733/734 numerics M (5 pts) Registry lookups ✅
labeled-response ircv3.net/specs/extensions/labeled-response-3.2 Client tags a request with label; server wraps the reply in a BATCH carrying that label. Composes with batch ✅ L (8 pts) batch ✅, message-tags ✅
sts ircv3.net/specs/extensions/sts Strict Transport Security: advertise a TLS+port policy to upgrade plaintext/WebSocket clients. Relevant once raw TCP :6697 is exposed (it is, per Phase 7 ✅) S (2 pts) dual transport ✅

Notes:

3.2 Draft / spec-pending extensions

Cap Spec What it adds Effort Depends on
draft/pre-away ircv3 draft Persist away status + message across reconnects; cross-device away sync. Noted out-of-scope in TICKET-063. M (3 pts) AccountStore ✅, services UserStore (§6)
draft/read-marker ircv3 draft Per-(user, channel) last-read marker, propagated via +draft/read-marker tag. The chathistory plumbing (TICKET-063) already keeps the marker locally; this ticket persists it across connections and shares it. M (5 pts) chathistory ✅, services UserStore
draft/multiline ircv3 draft BATCH +id draft/multiline :target for multi-line messages up to a byte budget. Clients (TheLounge, gamja) render as joined blocks. M (5 pts) batch ✅, message-tags ✅
draft/typing ircv3 client-behavior/typing `TAGMSG +draft/typing=active paused doneto channel members (excluding sender unlessecho-message`). Client UX only; no server state.

Notes:

3.3 Already-shipped caps blocked on adapter wiring

Worth calling out explicitly so they are not re-implemented by mistake:

Cap Status
account-tag Reducer emits; only meaningful once AccountStore is bound per adapter (TICKET-071 ✅).
extended-join Reducer emits the account realname form; same dependency as above.
draft/chathistory Reducer + in-memory store shipped (TICKET-063 ✅); adapter MessageStore wiring landed in TICKET-070 ✅. Persistent CF (D1/DO) and AWS (DynamoDB) stores are the next step.
msgid Standalone cap advertised and emitted on the live PRIVMSG/NOTICE fanout (TICKET-111 ✅). A single IdFactory.nonce() is shared between the live @msgid=<id> tag and the chathistory record so the two match for the same message.

4. Missing ISUPPORT tokens

packages/irc-core/src/commands/isupport.ts should grow:


5. Channel + user modes that arrive with services

The v1 mode set (o v b i k l t n m s p channel, i o w s user) gains:

Mode Type Meaning Source
r (channel) A Channel is registered with ChanServ services
R (channel) B Block join/message from unregistered nicks services
M (channel) B Mod speak requires registered nick (vs. +m = op) services
c (channel) B Block ANSI color (requires message inspection) optional
r (user) — User is identified to NickServ services
q (user, recv-only) — Protected from oper kick (U-line / services) services
S (user) — Connected via TLS (set by transport, read-only) transport

S is a near-freebie — the transport already knows whether TLS is in use (ConnectionActor accepts a transportMeta); surfacing it as a user mode costs one reducer branch.


6. IRC services

6.1 Architecture decision: integrated services (not pseudo-servers)

Two designs exist in the wild:

A. Pseudo-client services (Atheme / Anope model). A separate process connects as one or more virtual IRC clients named NickServ etc.; users interact via PRIVMSG NickServ :REGISTER hunter2 email. The link between ircd and services speaks a server-to-server protocol (TS6 / P10 / IRCv3 S2S).

B. Integrated services. Service names are reserved nicks recognized by the daemon at PRIVMSG time and routed to dedicated reducers backed by a ServicesStore port. No S2S, no separate process, no pseudo-client connection.

Decision: B — integrated services. Rationale:

Trade-off documented: integrated services are not binary-compatible with existing Atheme/Anope modules. Operators migrating from a services-aware network should expect to re-register nicks and channels. This is called out in docs/ when the work ships.

6.2 The ServicesStore port

/** Persists services-owned state. Mirrors the AccountStore/MessageStore pattern. */
export interface ServicesStore {
  // NickServ
  registerNick(account: string, passwordHash: string, email?: string): Promise<void>;
  verifyNick(account: string, password: string): Promise<boolean>;
  dropNick(account: string): Promise<void>;
  isRegisteredNick(account: string): Promise<boolean>;
  setNickEnforce(account: string, mode: 'none' | 'kill' | 'ghost'): Promise<void>;

  // ChanServ
  registerChannel(chan: string, founder: string): Promise<void>;
  dropChannel(chan: string): Promise<void>;
  getChannelFounder(chan: string): Promise<string | null>;
  setChannelAccess(chan: string, account: string, level: AccessLevel): Promise<void>;

  // HostServ
  setVhost(account: string, vhost: string): Promise<void>;
  getVhost(account: string): Promise<string | null>;

  // MemoServ
  recordMemo(to: string, from: string, body: string): Promise<void>;
  listMemos(account: string): Promise<Memo[]>;
  markMemoRead(account: string, id: string): Promise<void>;
}

Adapter implementations:

Adapter Backend
in-memory-runtime Maps, used by local-cli + tests
cf-adapter D1 (shared with the SASL store from TICKET-093 ✅) or a dedicated ServicesDO
aws-adapter DynamoDB Accounts table extended (TICKET-072 ✅ already provisions it) + new Channels/Memos tables

6.3 Services scope

Service In scope Out of scope (this pass)
NickServ REGISTER, IDENTIFY, LOGOUT, GHOST, DROP, INFO, SET ENFORCE/PASSWORD/EMAIL, link to SASL account, +r user mode, nick enforcement CertFP (covered by mTLS SASL EXTERNAL ✅), grouped nicks (alias list)
ChanServ REGISTER, IDENTIFY, DROP, SET FOUNDER/MLOCK/RESTRICTED/KEEPTOPIC, ACCESS/LEVELS (founder/sop/aop/vop/hop), +r/+R/+M channel modes, KEEPTOPIC persistence ACL inheritance, fantasy commands, OP/DEOP shorthand (defer)
HostServ ON/OFF/REQUEST/SET (oper), integrate with the existing cloak machinery (packages/irc-core cloaking shipped in Phase 5), +t user mode (vhost in use) per-channel vhosts
OperServ AKILL (network-wide ban, persisted + applied on connect), JUPE (block a nick/server name locally), RAW (oper-only, useful for recovery) STATS aggregation (TICKET-104 🔄 covers server stats), IGNORE list
MemoServ SEND, LIST, READ, DEL, FORWARD, send-on-identify (queued memos delivered at login) channel memos, group memos
BotServ — Out of scope entirely. Channel-joined bots are a runtime cost serverless deployments should not absorb by default. Operators can run their own bots as ordinary clients.
StatServ — Out of scope. STATS (TICKET-104) + observability dashboards (TICKET-045 ✅) cover it.

6.4 Nick enforcement policy

NickServ SET ENFORCE <mode> decides what happens when an unidentified client uses a registered nick:

Implemented as a new EnforceNick effect emitted from the registration reducer when the chosen nick matches a registered account the connection is not identified as. The dispatch layer resolves it against runtime.disconnect(...).

6.5 Services ↔ daemon hooks

Already-present seams the services reuse:


7. Proposed tickets

Ticket numbers are placeholders starting at TICKET-110 (next free id per todo.md). Final ids assigned at ratification. Each ticket follows strict TDD (Red → Green → Refactor) per CLAUDE.md / AGENTS.md.

Phase 12 — IRCv3 extensions

TICKET-110 — IRCv3 account-notify (push ACCOUNT on login/logout)

TICKET-111 — IRCv3 msgid (emit msgid tag on every message)

TICKET-112 — IRCv3 standard-replies (FAIL/WARN/NOTE)

TICKET-113 — IRCv3 MONITOR (modern ISON replacement)

TICKET-114 — IRCv3 labeled-response (correlate request→reply via BATCH)

TICKET-115 — IRCv3 sts (Strict Transport Security policy)

TICKET-116 — IRCv3 draft/typing (typing indicator broadcast)

TICKET-117 — IRCv3 draft/multiline (multi-line BATCH)

TICKET-118 — IRCv3 draft/read-marker (persistent last-read)

TICKET-119 — IRCv3 draft/pre-away (persist away status)

TICKET-120 — ISUPPORT tokens for new caps + modes

TICKET-121 — User mode S (TLS connected)

Phase 13 — IRC services

TICKET-122 — ServicesStore port + in-memory reference impl

TICKET-123 — NickServ: REGISTER / IDENTIFY / DROP / INFO + +r

TICKET-124 — NickServ: nick enforcement (SET ENFORCE)

TICKET-125 — ChanServ: REGISTER / DROP / SET + +r/+R/+M

TICKET-126 — ChanServ: ACCESS / LEVELS (SOP/AOP/VOP/HOP)

TICKET-127 — HostServ: ON / OFF / REQUEST / SET (oper)

TICKET-128 — OperServ: AKILL / JUPE / RAW

TICKET-129 — MemoServ: SEND / LIST / READ / DEL + queue delivery

TICKET-130 — Adapter backends for ServicesStore

TICKET-131 — Docs: services reference + admin tooling


8. Sequencing

Recommended landing order, respecting dependencies:

  1. Cheap IRCv3 wins first. TICKET-110 (account-notify), TICKET-111 (msgid), TICKET-115 (sts), TICKET-121 (+S mode), TICKET-116 (draft/typing). All small, all compose with shipped work, all unblock better client UX.
  2. MONITOR + standard-replies. TICKET-113, TICKET-112.
  3. labeled-response. TICKET-114 — largest IRCv3 item, do last among the stable caps; touches every reducer that emits a reply.
  4. Services foundation. TICKET-122 (port + in-memory store).
  5. NickServ core. TICKET-123, TICKET-124 — needed before ChanServ because every other service keys off the identified account.
  6. ChanServ. TICKET-125, TICKET-126.
  7. HostServ / OperServ / MemoServ. TICKET-127, TICKET-128, TICKET-129 (parallelizable).
  8. Persistent adapter backends. TICKET-130 (unblocks any production use of services).
  9. Draft caps that depend on services. TICKET-118 (draft/read- marker), TICKET-119 (draft/pre-away).
  10. draft/multiline. TICKET-117 — independent, can land anywhere after the IRCv3 cheap wins.
  11. Docs + ISUPPORT catch-up. TICKET-131, TICKET-120.

9. Risks

Risk Mitigation
labeled-response plumbing touches every reducer Land BATCH wrapping first (already shipped); add label propagation as a ctx field and a single dispatch hook. Refactor incrementally per reducer.
Integrated services drift from user expectations (Atheme parity) Document the decision up front (TICKET-131); keep command names/behaviour aligned where it's free.
Nick enforcement races (NICK + IDENTIFY interleaved) All mutations through the single ServicesStore authority per account; the EnforceNick effect is emitted after reserveNick succeeds, so the nick is owned before enforcement fires.
Services-backed caps (draft/read-marker, draft/pre-away) coupled to services launch Track as Phase 12 tickets with explicit Phase 13 dependencies; ship IRCv3 cheap wins independently.
D1/DynamoDB cost from per-message msgid recording msgid is emitted on the live line without persisting; only MessageStore records (chathistory) hit storage. Same cost profile as today.
draft/multiline byte budget abuse Hard cap enforced in the reducer (FAIL over-budget); per-connection flood control (TICKET-065 ✅) bounds the burst.

10. Definition of done (per phase)

Phase 12 — IRCv3 extensions is done when:

Phase 13 — IRC services is done when:


11. Open questions