ADR-009: Dual transport — wss (default) + irc+tls on :6697
Date: 2026-07-26 (Phase 7) Status: Accepted Supersedes: ADR-005 (wss-only transport in v1) Superseded by: —
Context
ADR-005 chose wss-only for v1. That decision rested on four points, all still accurate as facts about the serverless substrates:
- Both target platforms (CF Workers, AWS API Gateway WebSocket) support wss natively with zero additional infrastructure; neither can accept raw TCP from its request-response compute (Workers is request-response; API Gateway is WebSocket-only).
- wss is TLS-terminated at the edge (CF) or at API Gateway (AWS), so the Lambda/Worker never sees plaintext credentials.
- One IRC message per WebSocket text frame eliminates the need for a
stateful byte-stream line buffer in the core (the parser already tolerates
\r\n-joined frames for robustness). - The serverless cost/ops story stays clean: no long-running origin, no TCP keepalive management, one codebase + two adapters.
What changed the decision is demand, not capability:
- RFC 1459/2812 compliance. The target modern clients (WeeChat, HexChat,
IRCCcd, TheLounge) mostly support wss, but there is a long tail of older
clients, bots, bridges, and monitoring tools that only speak
irc+tls(raw TCP + TLS on :6697). Supporting them was a non-goal for the initial v1 cut; the gap now matters for the compatibility-sweep deliverable. - SASL EXTERNAL (mTLS).
AUTHENTICATE EXTERNALneeds the client certificate to reach the application layer (ADR-006). With wss and edge-terminated TLS, the cert is consumed at the edge and the app only gets a verified subject via platform config (CF API Shield mTLS, API Gateway custom-domain mTLS — both TICKET-054). A realirc+tlspath gives a more conventional mTLS surface that matches client expectations. - Marginal cost is small. The core is transport-agnostic by design: pure
reducers + a
ConnectionActorthat already accepts\r\n-joined frames. Adding a second transport is a second per-platform entry-point adapter, not a re-architecture. The generalized TCP byte-stream transport seam is TICKET-053; the per-platform adapters are TICKET-055 (CF) and TICKET-056 (AWS).
Decision
v1.x adopts a dual-transport model:
wss stays the serverless default.
wss://server.tld/with one IRC message per WebSocket text frame remains the primary path. It needs zero extra infrastructure on either platform and keeps the clean serverless story for the common case.irc+tlson :6697 is added as a second, per-platform adapter for RFC-compliant clients. TLS is mandatory; plaintext TCP on :6667 stays unsupported. The entry point differs per platform because neither serverless compute can accept raw TCP directly:Platform Edge (TLS termination) Origin / compute CF Cloudflare Spectrum (Enterprise) Cloudflare Container running TCP byte-stream ConnectionActorover a persistent runtimeAWS Network Load Balancer ( tlslistener)Lambda streaming function (target); connection ID derived from NLB flow metadata Spectrum terminates TLS at the edge and forwards plaintext TCP to the container origin; NLB terminates TLS and invokes a Lambda streaming function as target. Both reuse
AwsRuntime/the CF runtime storage layer for connection/channel/nick state.
The pure reducer core and ConnectionActor are shared across both
transports and both platforms. Only two things differ on the new path:
- The transport seam (TICKET-053):
ConnectionActoraccepts either aWsTextFrameTransport(one message per frame — current behavior) or aTcpByteStreamTransport(stateful\r\nframing across chunks with partial-line buffering). Parser → reducer → dispatch is unchanged. - The entry-point adapter (TICKET-055 / TICKET-056): Spectrum+Container on CF, NLB+Lambda streaming on AWS.
mTLS (TICKET-054) is done at the edge on the wss path (CF API Shield mTLS / API Gateway custom-domain mTLS) and via the TLS handshake on the TCP path (Spectrum cert verification / NLB ACM cert + optional client-cert verification).
Consequences
Positive:
- RFC 1459/2812-compliant clients, bots, and bridges connect natively without
a
tcp-ws-forwarderbridge. This unblocks the compatibility-sweep deliverable for the long tail of TCP-only clients. - A conventional mTLS surface opens for SASL
EXTERNAL(TICKET-054): the client cert reaches the application via the TLS handshake on the TCP path, matching standard IRC client expectations, in addition to the edge-mTLS path on wss. - Core reuse: the pure reducers,
IrcRuntimeport, andConnectionActorare shared verbatim. The transport seam (TICKET-053) is the only new abstraction in the core; everything else is per-platform glue. - The wss default is untouched — deployments that do not need raw TCP pay no infra cost and keep the clean serverless story.
Negative:
- CF breaks the pure-serverless story on the TCP path. Spectrum is an Enterprise-tier feature, and the origin is a stateful Cloudflare Container (or external compute), not a Worker. Connection state must survive origin reconnects via the runtime's persistence layer — a new operational surface that the wss path (Durable Objects) does not have.
- AWS NLB + Lambda streaming has hard limits: Lambda streaming invoke idle timeout and maximum connection/duration caps. Long-idle IRC clients can be dropped; reconnect/PING behavior must tolerate this (same class of risk as the APIGW 2h WS cap, PLAN §10).
- Extra per-platform adapter surface. Two entry-point adapters (TICKET-055, TICKET-056) and a new transport contract dimension (TICKET-057) are added to the test matrix. Cross-adapter drift is now a transport × runtime matrix, mitigated by the parametrized contract suite.
- Double deploy/ops paths per platform. Each platform now ships a wss stack and a TCP+TLS stack with different infra (DO vs Container; APIGW vs NLB). Deploy docs, cost monitoring, and incident runbooks must cover both (TICKET-058).
References
- ADR-005 — wss-only transport in v1 (superseded by this ADR)
- ADR-006 — SASL mechanism scope (PLAIN only; EXTERNAL deferred to Phase 7; this ADR unblocks the mTLS path for EXTERNAL)
- PLAN §3 — Protocol scope (connection framing)
- PLAN §9 — Key technical decisions (framing)
- TICKET-052 — this ADR; revises the transport decision
- TICKET-053 — generalize
ConnectionActortransport seam (TCP byte-stream framing) - TICKET-054 — mTLS support (edge config +
AccountStoreplumbing; enables functional SASL EXTERNAL) - TICKET-055 — CF TCP+TLS adapter via Cloudflare Spectrum + Container origin
- TICKET-056 — AWS TCP+TLS adapter via Network Load Balancer (TLS) + Lambda streaming
- TICKET-057 — extend integration contract suite over transport (wss × TCP+TLS)
- TICKET-058 — docs:
Cloudflare-TCP-Deployment.mdandAWS-TCP-Deployment.md(TCP+TLS variants)