ADR-005: wss-only transport in v1 (no raw TCP)
Date: 2025-01-15 (Phase 1) Status: Superseded by ADR-009 (Phase 7 — TICKET-052) Supersedes: —
Context
IRC is traditionally spoken over raw TCP (port 6667 plaintext, 6697 TLS). WebSocket Secure (wss) is a framing layer over TLS that adds an HTTP upgrade handshake. Serverless compute platforms have fundamentally different support for these two transports:
- Cloudflare Workers can accept WebSocket upgrades but cannot accept raw TCP. Spectrum (Enterprise tier) can terminate TLS and proxy to an origin, but Workers itself is request-response.
- AWS API Gateway WebSocket API provides persistent WebSocket connections
with per-message Lambda invocation. Raw TCP requires a Network Load Balancer
- Lambda streaming (invoke mode) — a fundamentally different integration.
The v1 goal is a clean serverless story: one codebase, two adapters, zero stateful origin servers. Raw TCP on either platform requires additional, non-serverless infrastructure (a long-running container on CF; NLB + Lambda streaming on AWS).
Additionally, wss carries one IRC message per WebSocket text frame, which
simplifies framing: the server does not need to implement a stateful
byte-stream line buffer. The parser still tolerates \r\n-joined frames
(multiple messages in one text frame) for robustness, but the common case is
one-message-per-frame.
Decision
v1 ships wss-only. No plaintext TCP listener, no irc:// scheme, no
port 6667/6697. Clients connect via wss://server.tld/ and speak IRC inside
WebSocket text frames.
Rationale:
- Both target platforms support wss natively without additional infrastructure.
- wss is TLS-terminated at the edge (CF) or at API Gateway (AWS) — the Lambda/Worker never sees plaintext credentials.
- One-message-per-frame eliminates the need for a TCP-style line-framing buffer in the core (though the parser tolerates joined frames).
- The serverless cost/ops model is clean: no long-running origin, no TCP keepalive management.
The ConnectionActor transport seam accepts WebSocket text frames directly.
A Transport interface is not abstracted in v1 — the actor's
receiveTextFrame(text) is the only entry point.
Consequences
Positive:
- Zero infrastructure beyond the serverless platform. No TCP listener, no origin server, no port management.
- TLS everywhere — no accidental plaintext exposure.
- Simple framing: WebSocket text frames are message-delimited by definition.
- Lower operational burden for v1.
Negative:
- RFC 1459/2812 clients that only speak raw TCP cannot connect. WeeChat and HexChat support WebSocket; some older clients and bots do not.
- The WebSocket overhead (HTTP upgrade, per-frame masking) adds ~2–10 bytes per message vs. raw TCP. Negligible for IRC's small message sizes.
- Some IRCv3 extensions assume a TCP transport (e.g. SASL EXTERNAL via client certs over TLS passthrough). Without raw TCP, mTLS must be done at the edge (CF API Shield, API Gateway custom domain), which changes the cert verification surface.
Superseded: Phase 7 (TICKET-052) reverses this decision for v1.x: a
dual-transport model (wss stays the default; irc+tls on :6697 is added via
Cloudflare Spectrum + Container origin, and AWS NLB + Lambda streaming). This
ADR documents the original v1 rationale; the superseding ADR covers the
reversal and per-platform TCP+TLS mapping.
References
- PLAN §3 — "No plaintext TCP in v1 (wss only)"
- PLAN §9 — "Framing: wss-only"
- TICKET-052 — superseding ADR: dual wss + irc+tls
packages/irc-server/src/actor.ts—receiveTextFrame(text)(single transport entry point in v1)