Files
alkhttp/docs/architecture/decisions/067-websocket-carries-channels.md
T
glm-5.3-flash 320ea87b08 docs: port architecture specs and ADRs from alknet-http; write new alkhttp ADRs 067-070
Phase 1 (SDD) — architecture documentation:

Ported specs (adapted for alkcall, producer/consumer terms, 6-endpoint
gateway, channels-over-WS, Sub/Pub operation types):
- overview.md, http-server.md, http-adapters.md, http-mcp.md
- README.md index (rewritten for alkhttp)

New ADRs:
- 067: WebSocket carries the channels protocol (8-byte chunk demux,
  channel 0 = alk/call, upgrade path /alk/channels)
- 068: gateway /publish endpoint for Pub operations (NDJSON body)
- 069: WebTransport out of scope in alkhttp (alknet concern)
- 070: from_wss consumer adapter (wss feature, tokio-tungstenite)

Ported ADRs (25, same numbers, port notes + amendments where the
extraction changed facts): 001-004, 010, 014, 015, 017, 022, 023, 027,
034, 036, 037, 039, 041, 042, 044, 045, 046, 047, 048, 049, 051, 066.

websocket.md rewritten for the channels session; open-questions.md
seeded (OQ-01 WS byte-stream adapter, OQ-02 /publish framing,
OQ-03 from_wss reconnect, OQ-04 browser client ownership).

Verified: cargo test, clippy -D warnings, fmt, doc --no-deps.
2026-08-27 14:19:24 +00:00

7.6 KiB

ADR-067: WebSocket Carries the Channels Protocol

Status

Accepted

Context

The alknet design (ADR-044, ADR-048 there) made the WebSocket path a bare call-protocol session: one EventEnvelope JSON object per binary WS message, handed directly to the shared Dispatcher. That design predates the channels protocol — the 8-byte chunk multiplexer (alkcall ADR-034/035) with channel 0 pre-negotiated as alk/call (alkcall ADR-036) — and was specified when browsers needed only the call protocol.

alkhttp is extracted onto alkcall, where the call protocol and the channels protocol are one crate, one connection model, and one wire story. Two problems remain with the bare-envelope WS design:

  1. No data channels for browsers. A browser session over WS could reach the call protocol but could never open a data channel (TTY, tunnel, a WASM SSH client's transport). Every downstream protocol crate that rides channels would need a separate browser path.
  2. Two connection shapes, one stack. A browser WS session and a Rust in-line channels session (TCP+TLS) would be structurally different sessions at the dispatch layer: the browser one a raw envelope stream, the Rust one a channels connection with a ChannelManager. Hub code that wants to treat browser sessions and Rust spokes uniformly would branch on the transport.

Decision

The WebSocket path carries the channels protocol, not a bare envelope stream. A WS session is an in-line channels substrate (alkcall ADR-034 §substrate modes): the WS connection's binary message stream is the transport; the 8-byte chunk header demultiplexes N logical channels over it; channel 0 is pre-negotiated as alk/call (alkcall ADR-036) and carries the native call-protocol session — the shared Dispatcher runs on it unchanged.

Upgrade path

The default WS upgrade path is /alk/channels (was /alknet/call in the alknet design). The path is an axum route on the HttpAdapter router, subject to the same reserved-path collision rule as any default-surface route (ADR-046).

Framing: the WS message boundary carries chunks, not envelopes

The alknet design's "one EventEnvelope = one binary WS message, no length prefix" framing is superseded. The WS message boundary now carries channels chunks; the call protocol's envelopes ride inside channel 0 as length-prefixed JSON (alkcall ADR-014 frame format), the same as any other in-line channels transport.

Layering on the wire, for a call frame over WS:

WS binary message
└── chunk header [channel_id: u32 BE][length: u32 BE]     (8 bytes)
    └── payload = frame [len: u32 BE][EventEnvelope JSON] (channel 0)

A data channel's chunks are chunk header + opaque payload — the handler (TTY, tunnel, WASM client) owns its framing inside the payload, exactly as over TCP+TLS (alkcall ADR-035: no stream_type, the handler owns its sub-stream multiplexing).

The WS↔byte-stream adaptation (how the message-oriented WS stream presents as the byte stream the channels demux reads, and how the mux's byte writes become WS messages) is the implementation's core piece and its buffering semantics are tracked in OQ-01.

Dispatch: channel 0 = the shared Dispatcher, unchanged

On upgrade, the handler:

  1. Resolves the caller's identity from the Authorization: Bearer header via IdentityProvider::resolve_from_token() — the same auth path as any HTTP request (ADR-004). No token → 401. The resolved identity is carried on the session for observability and AccessControl.
  2. Wraps the WS stream as a Connection (Connection::from_bidi, ALPN alk/channels).
  3. Runs the channels accept path — alkcall's ChannelsAdapter in-line demux loop — installing channel 0 via the install_channel_zero hook: construct channel 0's CallConnection and run Dispatcher::run_loop_single_stream on it, exactly as the TCP+TLS in-line substrate does.
  4. Data channels (1..N) are routed to whatever the deployment registered as openable ALPNs — for a hub, the same openable ALPNs a Rust spoke can reach. The browser opens them via the per-ALPN open ops on channel 0 (alkcall ADR-047), the same mechanism as any consumer.

Everything ADR-048 says about dispatch — call.requestedDispatcher::dispatch_requested with AccessControl::check gating, call.responded/call.completed/call.aborted correlated by id via the pending map, text WS messages rejected with a protocol-level close — applies to channel 0 verbatim. The only framing change is the envelope's position in the layering (above).

Bidirectionality, overlay, browsers-are-not-peers: unchanged

  • Both sides can initiate calls on channel 0 (alkcall ADR-015's stream-agnostic correlation). The browser calls hub ops; the hub can call browser-registered ops over the same session.
  • Browser-registered ops land in the connection-local Layer 2 overlay (alkcall ADR-019) and die when the WS connection drops.
  • Browsers are not peers (ADR-034 §4): bearer token, no PeerId, not in the peer graph. The connection handle, not a PeerRef, is how the hub reaches browser ops.

What this gives the browser

A browser session is now structurally the same session as a Rust in-line channels session:

  • Call protocol operations (channel 0) — both directions.
  • Data channels for anything the deployment registers as openable — a WASM SSH client, a TTY, a tunnel — with no browser-specific protocol work.
  • The same discovery ops (services/list, services/schema) as any consumer.

Consequences

Positive:

  • One session shape across browser and Rust in-line transports; hub code is transport-blind.
  • Browsers get data channels for free, riding alkcall's channels machinery rather than a bespoke browser protocol.
  • The from_wss consumer adapter (ADR-070) becomes the mirror image of the server path: the same WS byte-stream adaptation, then alkcall's ChannelClient.
  • The WS framing delta from alknet is contained to alkhttp's WS adapter; the Dispatcher, registry, and channels layers are untouched (they are alkcall's).

Negative:

  • Browsers must speak the channels framing (8-byte header, channel 0 open ops) rather than bare JSON envelopes. The JS client for this lives outside alkhttp; the framing is small and the alkcall BAST document (chunk-header.bast.json) is the cross-language contract.
  • The WS↔byte-stream adapter is new, unproven code with backpressure and partial-write hazards (OQ-01).
  • Existing alknet-era WS clients (bare envelope per message) break. No such client ships outside the mono-repo; the break is intentional and one-way.

References

  • websocket.md — the full WS session spec
  • ADR-048 — the native session (not gateway shape) decision this ADR amends (channel 0 framing; upgrade path)
  • ADR-044 — WS as the browser bidirectional path (stands); deferral mechanics superseded by ADR-069
  • ADR-070 — the consumer-side mirror of this decision
  • alkcall ADR-014 (EventEnvelope framing), ADR-015 (stream model), ADR-034/035 (channels wire format, pure multiplexing), ADR-036 (channel 0 pre-negotiated alk/call), ADR-039 (ChannelsAdapter), ADR-043 (ChannelClient), ADR-047 (openable ALPNs are operations)
  • alkcall docs/architecture/channels-wire.md — the 8-byte chunk format and wire invariants