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.
This commit is contained in:
2026-08-27 14:19:24 +00:00
parent 28c521b2f3
commit 320ea87b08
42 changed files with 11275 additions and 1 deletions
@@ -0,0 +1,555 @@
# ADR-044: Defer h3/WebTransport; Browsers Use WebSocket
*Ported from alknet ADR-044 (Defer h3/WebTransport; Browsers Use WebSocket); re-targeted to alkhttp.*
## Status
Accepted (supersedes alknet ADR-038; parks alknet ADR-040, alknet ADR-043)
## Status amendment (alkhttp port)
- **§1 (WebSocket as the browser bidirectional path) stands** and is the
operative decision for this crate: the browser bidirectional path in
alkhttp is WebSocket (alkhttp ADR-048 defines what the WS session
carries).
- **The deferral mechanics are superseded by alkhttp ADR-069**:
WebTransport is not "deferred" in alkhttp — it is **removed from
alkhttp scope entirely** (an alknet concern). There is no `h3`
feature gate, no revival trigger, and no `webtransport.md` spec in
this crate. ADR-044's "defer, revive when the ALPN-stream-proxy use
case arrives" framing is an alknet posture and does not carry into
alkhttp.
- **alknet ADR-038, ADR-040, and ADR-043 are not ported to alkhttp.**
They remain alknet decision records (ADR-038 superseded by alknet
ADR-044; ADR-040/ADR-043 parked per alknet ADR-044). Nothing in this
crate implements or plans them; if WebTransport revives as an alknet
transport, the browser path through alkhttp remains WebSocket.
## Context
alknet ADR-038 brought `h3`/WebTransport into scope as a first-class HTTP
transport, framed against the "two-way door as deferral" anti-pattern
(alkcall ADR-032 §"What this framework is NOT"). alknet ADR-040 (the
ALPN-stream-proxy) and alknet ADR-043 (the bidirectional-substrate
reframing) extended it. Three ADRs, one crate-spanning spec
(`webtransport.md`), and a body of design work.
Working through the implementation path surfaced a different concern than the
one alknet ADR-038 was written to correct. alknet ADR-038 correctly rejected *deferral-
as-hedging*; the present decision is *deferral-as-scoping*, which
alkcall ADR-032 explicitly permits (a decision that "genuinely doesn't
need to be made yet because the use case isn't concrete" — scope
management, not door-type classification). The two must not be
confused. Three concrete findings drove the scope re-evaluation:
*(Findings 2 — the WebTransport standards/dependency-maturity analysis —
and the iroh-relay precedent below concern the alknet transport layer
where QUIC/TLS/h3 live. alkhttp carries no transport stack; it is
retained here for provenance of why the browser path is WebSocket.)*
### Finding 1 — the browser bidirectional path doesn't require WebTransport
The load-bearing use case for `h3`/WebTransport in v1 is **a browser reaching
the call protocol bidirectionally**. alknet ADR-043 §2 establishes that the call
protocol's bidirectionality applies unchanged over any bidirectional stream —
the `Dispatcher` is stream-agnostic (alkcall ADR-015). That property is not unique to
WebTransport streams. **WebSocket is a full-duplex, long-lived connection over
which either side can send framed messages**, and the call protocol's
`EventEnvelope` framing fits the WebSocket path cleanly (the
`call.requested`/`call.responded`/`call.completed`/`call.aborted`
exchange works over WebSocket with no protocol change — the same `Dispatcher`,
the same `PendingRequestMap`, the same correlation by request ID; see the
framing note below and alkhttp ADR-048's status amendment for the
current chunk/framing model).
What WebTransport gives *over* WebSocket — native multiplexed bidirectional
streams, datagrams, the "carry any ALPN as a stream" substrate framing
(alknet ADR-043) — is genuinely better engineering, but none of it is *required* for
the call protocol from a browser. The call protocol multiplexes multiple calls
over a single connection by request ID (alkcall ADR-015); it does not need
WebTransport's per-stream multiplexing. The substrate/proxy framing (alknet
ADR-040, alknet ADR-043) is the thing that *does* benefit from WebTransport's
stream model — and that use case is the speculative one (see Finding 3).
**Framing correction (alkhttp port):** the original Finding 1 stated
"one `EventEnvelope` = one WS binary message." In the current wire
model that is superseded: the WS message boundary carries **8-byte
chunks of the channels protocol**, and on channel 0 the call protocol's
`EventEnvelope` frames are carried as length-prefixed JSON (alkcall's
frame format, alkcall ADR-014) inside the chunk payload — not
one-envelope-per-WS-message. The load-bearing property (self-delimited
frames, correlation by request ID, no protocol change) is unchanged.
See alkhttp ADR-067 §framing and alkhttp ADR-048's status amendment.
### Finding 2 — WebTransport is a draft standard on an experimental dependency stack *(alknet concern)*
WebTransport over HTTP/3 is still an IETF draft (`draft-ietf-webtrans-http3`,
at `-07` at time of writing), not an RFC. The Rust implementation landscape is
correspondingly immature:
- `wtransport` (the reference read during research) is a complete
pure-Rust implementation, but its own README states it "is not considered
completely production-ready" and "may undergo changes as the WebTransport
specification evolves."
- The hyperium stack (`h3` + `h3-quinn` + `h3-webtransport` + `h3-datagram`)
fits the axum/hyper ecosystem more naturally (h3 produces `http::Request`
types that axum consumes directly, which is load-bearing for the spec's
"HTTP/3 requests go through the same axum `Router`" commitment), but h3's
own README says it is "still very experimental... API could change."
- A research spike would be needed to verify the hyperium stack's
server-side WebTransport API before committing to it — the axum-bridge
feasibility is the load-bearing claim and is not yet confirmed against
actual crate APIs, only against READMEs and design philosophy.
Either choice puts a draft-standard protocol and an experimental Rust
dependency on the security surface of alkhttp's first release. The `h3`
feature gate (alknet ADR-038) isolates the risk for non-browser-facing deployments,
but a browser-facing hub must enable it — so the risk is borne precisely by
the deployment shape that motivates having a browser path at all.
*(In alkhttp this risk analysis is historical: the crate never gains an
`h3` feature. WebTransport is removed from scope per alkhttp ADR-069.)*
### Finding 3 — the ALPN-stream-proxy is speculative; the call protocol is not
alknet ADR-040 (the ALPN-stream-proxy — a browser with a WASM parser for SSH/SFTP/git
reaching any ALPN handler via WebTransport) is the genuinely compelling
WebTransport use case. It is also the one that is *not* required for v1:
- The call protocol from a browser works over WebSocket (Finding 1).
- The downstream crates unlocked by completing alkhttp (the SSH, git,
SFTP crates) do not require WebTransport or the proxy. They expose their
ALPNs natively over QUIC; the proxy is a *browser reachability* feature
for those ALPNs, not a prerequisite for the ALPNs to exist. *(The
transports exposing those ALPNs are alknet concerns.)*
- The WASM parsers (the browser-side SSH/SFTP/git clients) are themselves
downstream artifacts not yet built. The proxy is only useful once a parser
exists to consume it.
The proxy is "useful, and cheap-on-top *if* WebTransport already exists" —
but WebTransport does not yet exist, and building it speculatively to enable
a proxy whose consumers do not yet exist is the scope inversion. *(And in
the alkhttp crate, it was never built at all — removed from scope per
alkhttp ADR-069.)*
### The iroh precedent *(alknet-adjacent, but the signal carries)*
iroh's own relay (`iroh-relay`, the DERP-equivalent that provides NAT traversal
fallback) chose **WebSocket (WSS)**, not WebTransport, for its fallback path.
This is a strong signal from a project whose entire design center is QUIC and
P2P connectivity: when the question was "what does a browser need to reach our
protocol bidirectionally," their answer was WSS, not WebTransport. Aligning
with that precedent is not cutting against competent practice — it is
matching it.
### Concrete prior art: `@alkdev/pubsub`
The WebSocket path is not speculative — there is working prior art in the
same workspace. The `@alkdev/pubsub` package (`/workspace/@alkdev/pubsub/`)
already has a WebSocket client (`event-target-websocket-client.ts`) and
server (`event-target-websocket-server.ts`) built on a generalized "event
target" abstraction with an `EventEnvelope { type, id, payload }` shape.
The call protocol's `EventEnvelope` was derived from this envelope
(refined with typed event names `call.requested`/`call.responded`/etc. and
structured payloads); the sibling `@alkdev/operations` package
(`/workspace/@alkdev/operations/`) shares the lineage and uses the
`path.do.op` (dot-separated) vs the call protocol's `path/to/op`
(slash-separated) convention — a minor, mechanical delta. Syncing the
pubsub/operations WebSocket client to the call protocol's envelope is a
small adjustment (~a day of work: the envelope shape, the event-name
typing, the path separator), not a from-scratch browser-client build.
This is why the WebSocket path opens doors quickly: the browser (and
Node) client is mostly already written. *(In the current wire model the
pubsub-style clients additionally speak the channels chunk framing on
the WS path — see the framing correction under Finding 1 and alkhttp
ADR-067.)*
### The tradeoff between two use cases, not "good enough for now"
It is worth being precise about *why* WSS is the right choice here, because
"good enough until it isn't" undersells the decision. The two browser-reach
use cases have different right tools:
- **The call protocol from a browser (bidirectional).** WSS is *genuinely
the right tool*, not a stopgap. The call protocol multiplexes by request
ID (alkcall ADR-015), not by stream — it does not need WebTransport's
per-stream multiplexing. A WebSocket is a full-duplex, long-lived,
framed-message channel; the call protocol's framing fits the WS path
cleanly (see the framing correction for the current chunk-based model).
For this use case, WebTransport's stream model is engineering
sophistication the call protocol has no use for. WSS is not "good
enough" — it is well-matched.
- **The generalized ALPN router/proxy (a browser reaching a non-call ALPN
— SSH/SFTP/git via WASM).** WebTransport's native multi-stream model is
*genuinely the right tool* here, and WSS is *probably worse* for it. A
browser reaching a non-call ALPN over WSS would have to multiplex
logical streams over one WS frame stream by application-level framing —
doable (alknet ADR-043 §"SSH/SFTP/git-over-WSS-from-a-browser is
technically possible"), but it re-implements at the application layer
what WebTransport gives at the transport layer. This is the use case
WebTransport was built for, and it is the speculative one (Finding 3) —
the consumers (WASM SSH/SFTP/git parsers) do not exist yet. *(In
alkhttp this second use case does not exist at all: WebTransport is
removed from scope, alkhttp ADR-069, and the ALPN-stream-proxy was
never in this crate.)*
So the original deferral was not "use the worse tool now, upgrade to the
better tool later." It was "use the right tool for the use case we *have*
(call protocol from a browser → WSS), and defer building the tool for the
use case we *don't have yet* (generalized ALPN proxy → WebTransport)."
In the alkhttp port, the second half of that sentence is closed outright:
there is no deferred WebTransport future in this crate — the crate's
browser bidirectional path is WebSocket, full stop (alkhttp ADR-069).
## Decision
### 1. Defer `h3`/WebTransport. Browsers reach the call protocol over WebSocket. *(§1 stands; deferral mechanics superseded by alkhttp ADR-069)*
The `h3` ALPN, the `h3` feature gate, and the WebTransport dependency stack
are **deferred** in the original alknet decision — not implemented in the
initial release. In **alkhttp** this resolves further: WebTransport is
**removed from the crate's scope entirely** (alkhttp ADR-069); there is no
`h3` ALPN, no `h3` feature, and no deferred revival in this crate. The
clause that stands unchanged is the browser path itself: a browser
connecting to a hub authenticates by bearer token and upgrades an
HTTP/1.1 or HTTP/2 request to WebSocket. The resulting full-duplex WS
connection carries the call protocol's `EventEnvelope` frames (in the
current wire model: length-prefixed JSON frames on channel 0 inside the
channels 8-byte chunk framing — see the framing correction under Finding
1 and alkhttp ADR-067/ADR-048). The browser is a bidirectional
call-protocol client over this connection, using the same `Dispatcher`
and `PendingRequestMap` as the `alk/call` QUIC path (alkcall ADR-015 —
stream-agnostic correlation; a WS message stream is just another
`BiStream`-satisfying transport, extending the stream-agnostic claim
from QUIC bidirectional streams to any framed full-duplex byte channel).
The original scope-decision framing (deferral-as-scoping, per alkcall
ADR-032) still describes why WS was chosen; the reversal trigger below
does not apply to alkhttp — the "revival" it describes would be an
alknet concern and would not reopen alkhttp's surface.
### 2. alknet ADR-038 is superseded by this ADR. *(alknet record; not ported)*
alknet ADR-038's core decision — that `h3` is in scope, not deferred — is
reversed by this ADR in alknet. alknet ADR-038's *correction* of the
"two-way-door-as-deferral" anti-pattern stands as a document (the
anti-pattern is real); its specific decision (h3 in scope now) is
superseded. alknet ADR-038 is marked Superseded in the alknet record. It
is **not ported to alkhttp**; this crate has no `h3` decision to record
browsers-transport work against.
### 3. alknet ADR-040 and ADR-043 are parked, not superseded. *(alknet records; not ported)*
alknet ADR-040 (the ALPN-stream-proxy) and alknet ADR-043 (the
bidirectional-substrate reframing) are **not superseded** in the alknet
record — their decisions are correct, and they revive unchanged when
WebTransport revives *as an alknet transport*. They are marked Proposed
with an amendment noting implementation is deferred per this ADR. In
alkhttp, neither is ported and neither has any footprint. The two
transfers that the original decision applied during deferment:
- **alknet ADR-043 §2 (call-protocol bidirectionality over WebTransport)
transfers to WebSocket unchanged.** WebSocket is full-duplex; the call
protocol's bidirectionality applies over a WS connection exactly as
alknet ADR-043 §2 describes for WebTransport. The browser case where
the client registers no ops remains a use-case scoping, not an
architectural limitation. *(This transfer is the part of ADR-044 that
is live in alkhttp — it is what alkhttp ADR-048 implements.)*
- **alknet ADR-043 §3 (the no-`PeerId` connection-local overlay)
transfers to WebSocket unchanged.** A browser over WSS has no `PeerId`
on the hub's side for the same reasons it has none over WebTransport
(see §5 below); the connection-local Layer 2 overlay applies
(alkcall ADR-019). The pattern is transport-agnostic.
What does *not* transfer to WebSocket is alknet ADR-040 (the
ALPN-stream-proxy) and alknet ADR-043 §4 (the non-call-ALPN substrate
mechanism). Those require WebTransport's stream model and revive with
it *(as alknet work; alkhttp has no such path)*. SSH/SFTP/git-over-WSS-
from-a-browser is technically possible (multiplex logical streams over
one WS frame stream) but is not specified here — it is the same
speculative use case that motivated deferring WebTransport, and it is
not needed for v1.
### 4. WebSocket is the browser bidirectional path; HTTP/1.1+HTTP/2 remain the one-directional projection.
alkhttp's browser-reachable surface is:
| Transport | Direction | Use case |
|-----------|-----------|----------|
| `http/1.1`, `h2` | one-directional (client→server) | HTTP clients (curl, axios, `fetch` for request/response); SSE for subscription streaming (alkhttp ADR-049) |
| WebSocket (over `http/1.1` or `h2` upgrade) | **bidirectional** | Browser call-protocol clients; the path that restores the call protocol's bidirectionality for browsers |
WebSocket is the surface that **restores the call protocol's
bidirectionality for browsers** (the role alknet ADR-043 §5 assigned to
WebTransport). The one-directional projection that alknet ADR-043 §5
names for HTTP/1.1+HTTP/2 stands unchanged. (The `h3` row from the
original table does not exist in alkhttp — WebTransport is removed from
scope, alkhttp ADR-069.)
### 5. Browsers over WebSocket are not peers — the rationale, stated.
alkhttp ADR-034 §4 (ported from alknet ADR-034 §4) established that a
browser over WebTransport is not a peer (no `PeerId`, no
`PeerCompositeEnv` entry). The same applies to a browser over WebSocket,
and the rationale — which alknet ADR-034 §4 states as a closure without
the supporting argument — is worth making explicit because it is the
load-bearing distinction:
**"Peer" means an addressable node in the call-protocol peer graph — a
stable `PeerId`, reachable via `PeerRef::Specific`, whose ops land in
`PeerCompositeEnv`, whose identity is stable across reconnects.** It
does *not* mean "any endpoint that exchanges calls during a live
session." A browser is the second thing but not the first, on three
concrete grounds:
1. **No stable cryptographic identity of its own.** A `PeerEntry` is
anchored to fingerprints (Ed25519, X.509) that *the peer* presents
and the local node pins. A browser presents a bearer token the *hub*
issued; the "identity" is the hub's bookkeeping for that token, not
something the browser owns or that could be pinned by another node.
There is nothing to put in `PeerEntry.fingerprints`.
2. **Ephemeral.** Close the tab → connection dies → the connection-local
Layer 2 overlay (alkcall ADR-019) dies with it. A `PeerEntry` keyed
to a browser would be a permanently-dead entry within seconds.
`PeerRef::Specific("browser-X")` from another node would route to
nothing.
3. **Not addressable from other nodes.** `PeerRef::Specific` resolves
through `PeerEntry``PeerId`. Another node has no way to reach
"the browser currently connected to hub-A"; the hub holds that
connection as a live `CallConnection` handle, not as a peer-graph
entry. The connection-local overlay is precisely the mechanism that
gives the browser bidirectional-call capability *without* peer-graph
membership.
This is the explicit closure of the "browser as peer" path, on both the
inbound (this section) and outbound (alkhttp ADR-034 §2) sides. The
browser is a **bidirectional call target during a live session**, not a
**peer-graph member**. The connection-local Layer 2 overlay (alkcall
ADR-019) is what makes the former possible without requiring the
latter.
This rationale applies transport-agnostically — to WebSocket, to
WebTransport (an alknet transport, out of alkhttp scope per alkhttp
ADR-069), and to any future browser transport. alkhttp ADR-034 §4 is
amended by reference to this section.
## Consequences
**Positive:**
- alkhttp's first release does not carry a draft-standard protocol or
an experimental dependency stack on its security surface. The browser
path uses WebSocket, a mature, well-understood, RFC 6455 protocol with
first-class axum support (`axum::extract::ws`).
- The axum-bridge research spike for h3/WebTransport is not on the
critical path. WebSocket upgrade over HTTP/1.1 or HTTP/2 is standard
axum territory.
- The downstream crates that alkhttp unblocks (SSH, git, SFTP) are not
blocked on WebTransport or the proxy. They expose their ALPNs natively
over QUIC *(an alknet transport concern)*; browser reachability for
them would be a WebTransport feature — and in the alkhttp crate tree,
that is out of scope (alkhttp ADR-069), not a deferred feature.
- The crate stays lean: no `h3`, no `wtransport`/hyperium h3 stack, no
WebTransport feature gate. The only browser-bidirectional dependency
is the WebSocket upgrade path.
**Negative:**
- alknet ADR-038, ADR-040, and ADR-043 are not implemented in this
crate and are not ported to it (see Status amendment). Their design
work is preserved in the alknet record only. A reader of alkhttp docs
must go to the alknet docs to find them; this ADR's status amendment
is the pointer.
- The ALPN-stream-proxy (alknet ADR-040) is not available anywhere in
the alkhttp surface. A browser cannot reach SSH/SFTP/git ALPNs
through this crate — it can reach the call protocol over WebSocket,
but not the non-call ALPNs. *(This is the alknet deferral; in
alkhttp it is a removal from scope, alkhttp ADR-069.)*
- WebSocket is a single stream; it lacks WebTransport's native
multi-stream multiplexing. For the call protocol this is fine
(correlation is by request ID, not by stream — alkcall ADR-015), and
WSS is the well-matched tool for that use case (see §"The tradeoff
between two use cases"). Where WebTransport's stream model would
matter is the ALPN-stream-proxy (alknet ADR-040) — the speculative
use case, which is out of alkhttp scope entirely (alkhttp ADR-069).
- The original ADR-044's "WebTransport restores bidirectionality"
framing (alknet ADR-043 §5) becomes "WebSocket restores
bidirectionality" — and in this crate that framing is final, not
provisional.
## Reversal *(superseded by alkhttp ADR-069 for this crate)*
The original decision reversed when a concrete deployment needed the
ALPN-stream-proxy — i.e., a real use case of a browser running a WASM
SSH/SFTP/git client to reach a non-call ALPN over WebTransport. At that
point, in the alknet record:
1. The research spike deferred there (verify the hyperium stack's
server-side WebTransport API and the axum-bridge feasibility) is run.
2. alknet ADR-038 / ADR-040 / ADR-043 are un-parked and implemented as
written, with the `webtransport.md` spec as the design.
3. The WebSocket browser path (this ADR's §4) is not removed — it
remains as the simpler browser path for deployments that don't need
WebTransport's stream model. The two coexist.
**In alkhttp this reversal path does not exist.** WebTransport is not
deferred here — it is removed from crate scope (alkhttp ADR-069). If a
WebTransport deployment is ever built, it is an alknet concern (a
transport/relay feature in the alknet tree); the alkhttp surface it
would front is the stable HTTP contract this crate publishes, and no
alkhttp ADR or feature gate anticipates it. The crate-surface one-way-
door reasoning in the original (an `h3` feature gate becoming part of a
published interface) is moot: alkhttp publishes no `h3` surface.
## Research note (for revival) *(alknet concern; recorded for provenance only)*
A note from the original record: `wtransport` (the reference
implementation read during initial research) is *probably not* the right
dependency choice, despite being a complete and readable implementation.
The load-bearing integration concern was that the `h3` handler must
route HTTP/3 requests through the same axum `Router` as `h2`/`http/1.1`,
and `wtransport` owns its own HTTP serving path — bridging its request
type into the `http::Request` axum consumes is cross-ecosystem adapter
work. The hyperium stack (`h3` + `h3-quinn` + `h3-webtransport`)
operates at the stream level and produces `http::Request` types
natively, which is a better fit for the axum integration — but its
server-side WebTransport API needs verification before commitment.
This research was **not** run, and in alkhttp it never will be: there
is no WebTransport revival in this crate (alkhttp ADR-069). The note is
preserved only because the original record kept it for the alknet-side
revival question.
## Assumptions
1. **The call protocol's `EventEnvelope` framing fits the WebSocket
path cleanly.** In the original framing: an `EventEnvelope` is a
self-delimited JSON object; one envelope per WS binary message. In
the current wire model this is amended (see the framing correction
under Finding 1 and the alkhttp ADR-048 status amendment): the WS
message boundary carries channels chunks (8-byte chunk header, alkcall
ADR-034/ADR-035), and channel 0 — pre-negotiated as `alk/call`
(alkcall ADR-036) — carries `EventEnvelope` frames as length-prefixed
JSON (alkcall ADR-014's frame format) inside the chunk payload. The
load-bearing property — self-delimited frames, no streaming
deserializer across frame boundaries, correlation by request ID — is
unchanged. This is already verified by prior art: the
`@alkdev/pubsub` WebSocket client/server
(`/workspace/@alkdev/pubsub/src/event-target-websocket-client.ts`,
`event-target-websocket-server.ts`) carries the same
`{ type, id, payload }` envelope over WS binary messages; the call
protocol's `EventEnvelope` is a refined superset of that shape (typed
event names, structured payloads).
2. **WebSocket upgrade over HTTP/1.1 or HTTP/2 is supported by the
axum/hyper stack natively.** `axum::extract::ws` provides the upgrade
handler; the underlying connection is the same hyper HTTP connection
the `h2`/`http/1.1` handler already drives. No new framing library is
needed.
3. **A browser over WebSocket has the same peer-model properties as a
browser over WebTransport.** No `PeerId`, no `PeerCompositeEnv`
entry, connection-local Layer 2 overlay (alkcall ADR-019; alkhttp
ADR-034 §2). The rationale in §5 is transport-agnostic and applies
identically to WSS.
4. **The downstream crates (SSH, git, SFTP) do not require WebTransport
or the ALPN-stream-proxy to exist.** They expose their ALPNs natively
over QUIC *(an alknet transport concern)*; the proxy is a browser-
reachability feature, not a prerequisite for the ALPNs themselves.
Browser reachability for non-call ALPNs is the speculative use case;
in alkhttp it is not deferred but out of scope (alkhttp ADR-069).
## References
- alkcall ADR-032 §"What this framework is NOT" — the anti-pattern
alknet ADR-038 was written to correct; the original decision relies
on the explicit distinction between deferral-as-hedging (rejected)
and deferral-as-scoping (permitted: a decision that "genuinely
doesn't need to be made yet because the use case isn't concrete" —
scope management, not door-type classification). *(Port note: the
one-way-door decision framework lives in the alkcall crate as
alkcall ADR-032 — see the alkcall crate docs; alkhttp did not port
it. The original linked alknet ADR-009 by relative path.)*
- alknet ADR-038 — **superseded by this ADR (in alknet).** Its
correction of the two-way-door-as-deferral anti-pattern stands; its
specific decision (h3 in scope now) is reversed. **Not ported to
alkhttp.**
- alknet ADR-040 — **parked, not superseded (alknet record).** Revives
unchanged when WebTransport revives as an alknet transport. The proxy
is the speculative use case. **Not ported to alkhttp.**
- alknet ADR-043 — **parked, not superseded (alknet record).** §2
(bidirectionality) and §3 (no-`PeerId` overlay) transfer to WebSocket
unchanged; §4 (non-call-ALPN substrate) and §5's WebTransport-specific
framing revive with WebTransport as alknet work. **Not ported to
alkhttp.**
- alkhttp [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) §4 —
browsers are not peers; this ADR's §5 states the rationale
(addressability vs. bidirectionality) that the original §4 closes
without arguing. alkhttp ADR-034 §4 is amended by reference to this
ADR's §5.
- alkcall ADR-015 — call-protocol stream model; stream-agnostic
correlation, `Dispatcher`/`PendingRequestMap`; a WebSocket message
stream is another `BiStream`-satisfying transport. The call protocol
multiplexes by request ID, not by stream.
- alkcall ADR-014 — the call protocol's hand-rolled `EventEnvelope`
framing (length-prefixed JSON); the frame format now carried on
channel 0 of the WS path.
- alkcall ADR-034 / ADR-035 — the channels wire format (8-byte chunk
header); the multiplexing layer the WS path now carries (alkhttp
ADR-067).
- alkcall ADR-036 — channel 0 is pre-negotiated `alk/call`.
- alkhttp ADR-067 — the WS session carries the channels protocol; the
amendment that defines the current framing on this crate's WS path.
- alkhttp ADR-069 — WebTransport removed from alkhttp scope entirely.
- alkhttp ADR-049 — streaming handler for subscription operations (the
SSE projection for the HTTP one-directional path).
- alkhttp ADR-048 — the WS session shape (native session, not gateway);
the implementer-facing rule this ADR's §1 implied.
- alkhttp ADR-001 / ADR-002 — ALPN-based dispatch and the
`ProtocolHandler` trait (`HttpAdapter` on `h2`/`http/1.1`); the WS
upgrade layers on the same HTTP surface.
- The alkcall crate docs — the call-protocol spec and `EventEnvelope`
shape (see the alkcall crate's own documentation; the old
`call-protocol.md` relative link pointed into the alknet mono-repo
spec tree).
## Port notes
- **Superseded in part, per the status amendment above:** the "deferred"
framing is superseded by alkhttp ADR-069 (WebTransport removed from
alkhttp scope entirely). §1's WebSocket-as-browser-path stands; the
reversal trigger, the "revival" mechanics, the `webtransport.md` spec
pointer, and the research-spike-for-revival posture are alknet-record
content retained for provenance and annotated. alknet ADR-038/040/043
are not ported to alkhttp.
- **Framing correction:** the original assumed "one `EventEnvelope` = one
WS binary message." In the current wire model the WS message boundary
carries 8-byte channels chunks (alkcall ADR-034/ADR-035); channel 0 is
pre-negotiated `alk/call` (alkcall ADR-036) and carries `EventEnvelope`
frames as length-prefixed JSON (alkcall ADR-014) inside the chunk
payload. The original sentences asserting one-envelope-per-WS-message
are annotated inline (Finding 1 correction, Assumption 1) rather than
silently rewritten; the operative statement for this crate is alkhttp
ADR-067.
- Renames: "alknet-http" → "alkhttp"; the QUIC call ALPN "alknet/call" →
"alk/call" (alkcall ADR-004 `alk/` convention); "alknet ADR-012" (the
old call-protocol stream model) is cited as **alkcall ADR-015**
(alkcall ADR numbering differs from alknet numbering — alkcall ADR-012
is `ConnectionCredentials`, not the stream model).
- The original's references to `crates/http/webtransport.md` and
`crates/http/http-server.md` are alknet spec-tree artifacts; the
`webtransport.md` spec does not exist in alkhttp (no deferred
WebTransport spec is carried in this crate). The alkhttp equivalent of
the http-server spec work is the `websocket`/`server` subsystem of
this crate and alkhttp ADR-048.
- The `@alkdev/pubsub` prior-art references are kept as absolute
workspace paths (the packages still exist at those locations); the
framing delta note is annotated per the channels-chunk correction.
- The original table in §4 had an implied `h3` row (WebTransport →
alk/http in the alknet ALPN table); that row is dropped from the
alkhttp transport table per alkhttp ADR-069, with a note in §4.
- iroh-relay precedent: kept — it is a design-signal argument, not a
dependency claim; alkhttp has no iroh dependency.
- Original title preserved: "Defer h3/WebTransport; Browsers Use
WebSocket". The title's "defer" is the alknet-record posture; in
alkhttp the operative reading is "no WebTransport in this crate;
browsers use WebSocket" (alkhttp ADR-069).