docs(http): defer h3/WebTransport (ADR-044); browsers use WebSocket for v1
Working through the WebTransport implementation path surfaced a scope question distinct from the hedging-as-deferral anti-pattern ADR-038 was written to correct. Three findings drove the re-evaluation: 1. The browser bidirectional call-protocol path doesn't require WebTransport — WebSocket is full-duplex, EventEnvelope fits a WS binary message boundary cleanly, and the Dispatcher is stream- agnostic (ADR-012). What WebTransport gives over WebSocket (native multi-stream multiplexing, the ALPN-as-stream substrate) benefits the proxy use case, not the call protocol. 2. WebTransport is a draft standard (-07, not RFC) on an experimental Rust dependency stack (wtransport/h3 both self-describe as not production-ready). Either choice puts a draft protocol on the security surface of the first release. 3. The ALPN-stream-proxy (ADR-040) is speculative — its WASM parser consumers (browser SSH/SFTP/git clients) don't exist yet, and the downstream crates WebTransport deferral blocks (SSH, git, SFTP) expose their ALPNs natively over QUIC regardless. This is a scope decision (per ADR-009: a decision that 'genuinely doesn't need to be made yet because the use case isn't concrete'), not hedging. The reversal trigger is concrete: a real deployment needing the ALPN-stream-proxy. ADR-038 is superseded (its anti-pattern correction stands; its specific 'h3 in scope now' decision is reversed). ADR-040 and ADR-043 are parked, not superseded — their designs revive unchanged when WebTransport revives, with §2 (bidirectionality) and §3 (no-PeerId overlay) of ADR-043 transferring to WebSocket for v1. ADR-044 §5 also states the 'browser is not a peer' rationale that ADR-034 §4 closed without arguing: peer = addressable node in the call-protocol peer graph (stable PeerId, PeerRef::Specific-reachable, identity stable across reconnects), not 'any endpoint that exchanges calls during a live session.' A browser is the second but not the first (no stable crypto identity of its own, ephemeral, not addressable from other nodes). ADR-034 §4 and Assumption 2 are amended by reference. The wtransport-vs-hyperium dependency question is recorded (not resolved — WebTransport is deferred) in ADR-044 §'Research note' and webtransport.md so the revival doesn't re-derive it: wtransport probably isn't the right choice (axum-bridge friction — it owns its own HTTP serving path); the hyperium stack (h3 + h3-quinn + h3-webtransport) fits the axum integration better but its server-side WebTransport API needs verification before commitment. Reviewed by architecture-review subagent; all critical cross-reference issues (ADR-034 §5 stale 'in scope' assertion, ADR-036 Context listing h3 as implemented, webtransport.md Design Decisions table) resolved.
This commit is contained in:
@@ -203,8 +203,8 @@ A browser reaching a hub over WebTransport (or HTTPS) is served by the
|
||||
hub's `alknet-http` handler. The browser authenticates by **bearer
|
||||
token** (HTTP `Authorization`), resolved by the hub's
|
||||
`IdentityProvider::resolve_from_token` against the hub's
|
||||
`PeerEntry.auth_token_hash` or `ApiKeyEntry`. The browser is **not** an
|
||||
alknet peer on the hub's side either — it does not get a `PeerId`, does
|
||||
`PeerEntry.auth_token_hash` or `ApiKeyEntry`. The browser is **not an
|
||||
alknet peer on the hub's side either** — it does not get a `PeerId`, does
|
||||
not enter `PeerCompositeEnv`, and its "ops" are HTTP routes / WebTransport
|
||||
streams served by `alknet-http`, not entries in the call-protocol
|
||||
peer-keyed overlay. The hub's `PeerEntry` for the browser (if any) is
|
||||
@@ -214,6 +214,29 @@ This keeps the peer graph populated only by full alknet nodes (role 3
|
||||
hubs and role-3-style spoke nodes), never by browsers or pure HTTP
|
||||
clients.
|
||||
|
||||
> **Amendment (rationale added by
|
||||
> [ADR-044](044-defer-webtransport-browsers-use-websocket.md) §5):** The
|
||||
> closure above is correct but states the conclusion without the
|
||||
> supporting argument. The distinction that makes it correct is:
|
||||
> **"peer" in alknet 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 (it presents a bearer token the hub issued; nothing to
|
||||
> pin), (2) ephemeral (close the tab → connection dies → the
|
||||
> connection-local overlay dies with it; a `PeerEntry` keyed to a browser
|
||||
> would be dead within seconds), (3) not addressable from other nodes
|
||||
> (another alknet node has no way to reach "the browser currently
|
||||
> connected to hub-A"; the hub holds it as a live `CallConnection`
|
||||
> handle, not a peer-graph entry). The connection-local Layer 2 overlay
|
||||
> (ADR-043 §3, the inbound mirror of §2 above) is what gives the browser
|
||||
> bidirectional-call capability *without* peer-graph membership. This
|
||||
> rationale is transport-agnostic — it applies to WebSocket (the v1
|
||||
> browser path, ADR-044) and to WebTransport (when it revives) equally.
|
||||
> See ADR-044 §5 for the full statement.
|
||||
|
||||
### 5. WebTransport relay-as-proxy is a transport-only feature, scoped separately
|
||||
|
||||
A **WebTransport proxy** that terminates the browser's WebTransport
|
||||
@@ -237,12 +260,15 @@ is a real feature, especially for the browser-to-P2P-peer case. It is
|
||||
> WebTransport lands." That framing was a residual of the "two-way door
|
||||
> as deferral" anti-pattern (ADR-009 §"What this framework is NOT")
|
||||
> that [ADR-038](038-http3-and-webtransport-as-first-class.md) was later
|
||||
> written to reject — `h3`/WebTransport is a first-class transport, in
|
||||
> scope, not deferred. The *auth-model* decision in this §5 (the proxy
|
||||
> is transport-only; it does not change identity resolution) is
|
||||
> unchanged. The *scope* question (does the proxy belong in
|
||||
> `alknet-http` or a separate relay crate?) is tracked as OQ-38 — a
|
||||
> genuine scope question, not a deferral.
|
||||
> written to reject. ADR-038 has since been **superseded by
|
||||
> [ADR-044](044-defer-webtransport-browsers-use-websocket.md)**, which
|
||||
> re-defers `h3`/WebTransport as a genuine scope decision (the browser
|
||||
> bidirectional path uses WebSocket; WebTransport revives when a concrete
|
||||
> ALPN-stream-proxy use case arrives). The *auth-model* decision in this
|
||||
> §5 (the proxy is transport-only; it does not change identity
|
||||
> resolution) is unchanged by either ADR. The *scope* question (does the
|
||||
> proxy belong in `alknet-http` or a separate relay crate?) is tracked
|
||||
> as OQ-38 — a genuine scope question, not a deferral.
|
||||
|
||||
### 6. On-chain / smart-contract peer discovery fits the OQ-36 adapter pattern
|
||||
|
||||
@@ -340,11 +366,17 @@ It is noted here only to confirm it does not reopen OQ-37.
|
||||
intended — it is the same model iroh uses.
|
||||
|
||||
2. **Browsers never enter the peer-keyed overlay.** A browser is
|
||||
served by `alknet-http` (HTTP routes / WebTransport streams) and
|
||||
authenticates by bearer token. The hub may have a `PeerEntry` for
|
||||
the browser's token (to authorize it), but the browser is not a
|
||||
`PeerId`-bearing peer. This is the explicit closure of the
|
||||
"browser as peer" path — browsers are clients, not peers.
|
||||
served by `alknet-http` (HTTP routes / WebTransport streams /, per
|
||||
ADR-044, WebSocket) and authenticates by bearer token. The hub may
|
||||
have a `PeerEntry` for the browser's token (to authorize it), but the
|
||||
browser is not a `PeerId`-bearing peer. This is the explicit closure
|
||||
of the "browser as peer" path — browsers are clients, not peers.
|
||||
**The rationale** (addressability vs. bidirectionality — a browser
|
||||
has no stable identity of its own, is ephemeral, and is not
|
||||
addressable from other nodes) is stated in
|
||||
[ADR-044](044-defer-webtransport-browsers-use-websocket.md) §5, which
|
||||
amends §4 above by reference. The closure applies transport-
|
||||
agnostically.
|
||||
|
||||
3. **X.509 fingerprint pinning is only for known hubs.** Pinning an
|
||||
X.509 fingerprint for an arbitrary public API is brittle (cert
|
||||
@@ -395,13 +427,19 @@ It is noted here only to confirm it does not reopen OQ-37.
|
||||
repo/adapter pattern (trait in core, adapter additive in a separate
|
||||
crate)
|
||||
- `docs/research/alknet-http/phase-0-findings.md` — DH-2 (h3 /
|
||||
WebTransport; the original "deferred past v1" framing is rejected by
|
||||
ADR-038); the WebTransport-relay-as-proxy feature noted in this ADR's
|
||||
§5 is a transport-only feature whose scope is tracked as OQ-38
|
||||
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — `h3` /
|
||||
WebTransport is a first-class transport, not deferred (amends the
|
||||
"deferral bucket" wording in this ADR's §5; the auth-model decision
|
||||
stands)
|
||||
WebTransport; the original "deferred past v1" framing was rejected by
|
||||
ADR-038, which is now itself superseded by
|
||||
[ADR-044](044-defer-webtransport-browsers-use-websocket.md) — a genuine
|
||||
scope deferral); the WebTransport-relay-as-proxy feature noted in this
|
||||
ADR's §5 is a transport-only feature whose scope is tracked as OQ-38
|
||||
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — **superseded
|
||||
by [ADR-044](044-defer-webtransport-browsers-use-websocket.md)**.
|
||||
ADR-038 amended the "deferral bucket" wording in this ADR's §5 (the
|
||||
auth-model decision stands); ADR-044 reverses ADR-038's "h3 in scope
|
||||
now" decision as a scope deferral (the browser bidirectional path
|
||||
uses WebSocket; WebTransport revives when a concrete ALPN-stream-proxy
|
||||
use case arrives). The "browser is not a peer" closure in §4 above is
|
||||
amended by ADR-044 §5 with the addressability rationale.
|
||||
- `docs/research/references/iroh/iroh/04-sub-crates.md` — iroh's
|
||||
transport relay (`iroh-relay`), referenced to distinguish it from
|
||||
alknet's hub role
|
||||
|
||||
@@ -7,7 +7,8 @@ Proposed
|
||||
## Context
|
||||
|
||||
`alknet-http` implements `ProtocolHandler` for the standard HTTP ALPNs (`h2`,
|
||||
`http/1.1`, `h3`). An inbound HTTP request that targets an alknet operation
|
||||
`http/1.1`; `h3`/WebTransport is deferred per
|
||||
[ADR-044](044-defer-webtransport-browsers-use-websocket.md)). An inbound HTTP request that targets an alknet operation
|
||||
must become a call-protocol `call.requested` dispatch — the HTTP handler is a
|
||||
*projection* of the call protocol, not a parallel routing layer. The
|
||||
question is how an HTTP request maps to an operation invocation.
|
||||
@@ -96,9 +97,11 @@ A `Subscription` operation served over HTTP/1.1 or HTTP/2 projects its
|
||||
`call.responded` stream as Server-Sent Events. Each `call.responded` event
|
||||
becomes an SSE `data:` frame; `call.completed` closes the SSE stream;
|
||||
`call.aborted` closes the stream with an SSE error event. This is the
|
||||
HTTP/1.1 + HTTP/2 streaming projection. Over WebTransport (`h3`), the
|
||||
subscription projects directly onto a WebTransport bidirectional stream —
|
||||
no SSE framing is needed (see ADR-038 for the WebTransport path).
|
||||
HTTP/1.1 + HTTP/2 streaming projection. Over WebSocket (the v1 browser
|
||||
bidirectional path, ADR-044), the subscription projects directly onto the
|
||||
WS connection — `call.responded` events as binary WS messages, no SSE
|
||||
framing. WebTransport (`h3`) would project onto WebTransport bidirectional
|
||||
streams but is deferred per ADR-044.
|
||||
|
||||
### Auth
|
||||
|
||||
|
||||
@@ -2,7 +2,24 @@
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
**Superseded by [ADR-044](044-defer-webtransport-browsers-use-websocket.md).**
|
||||
|
||||
This ADR's *correction* of the "two-way-door-as-deferral" anti-pattern
|
||||
(ADR-009 §"What this framework is NOT") stands as a document — the
|
||||
anti-pattern is real, and the reasoning that rejected deferral-as-hedging is
|
||||
correct. However, this ADR's *specific decision* — that `h3`/WebTransport is
|
||||
in scope now, not deferred — is reversed by ADR-044. ADR-044 is a
|
||||
**scope** decision (permitted by ADR-009: "not needed for the current
|
||||
scope"), not a hedging deferral: the browser bidirectional path uses
|
||||
WebSocket (RFC 6455, mature, native axum support), the ALPN-stream-proxy
|
||||
(ADR-040) is the speculative use case whose deferral is the reversal
|
||||
trigger, and the draft-standard + experimental-deps surface area is not
|
||||
justified by a concrete v1 requirement.
|
||||
|
||||
ADR-040 and ADR-043 are **parked, not superseded** — their designs revive
|
||||
unchanged when WebTransport revives. See ADR-044 for the full scope rationale,
|
||||
the reversal trigger, and the research note on the wtransport-vs-hyperium
|
||||
dependency choice (recorded for the revival so it is not re-derived).
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -2,7 +2,19 @@
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
**Proposed — implementation deferred per [ADR-044](044-defer-webtransport-browsers-use-websocket.md).**
|
||||
|
||||
This ADR's decision is correct and is not superseded. It revives unchanged
|
||||
when WebTransport revives. ADR-044 defers `h3`/WebTransport as a scope
|
||||
decision (the browser bidirectional path uses WebSocket for v1; the
|
||||
ALPN-stream-proxy is the speculative use case whose concrete need is the
|
||||
reversal trigger). The proxy is the primary WebTransport-specific feature —
|
||||
it requires WebTransport's stream model and does not transfer to WebSocket.
|
||||
When a real deployment needs a browser running a WASM SSH/SFTP/git client to
|
||||
reach a non-call ALPN, this ADR is un-parked and implemented as written.
|
||||
|
||||
The `webtransport.md` spec is kept intact and marked `deferred` so the
|
||||
revival is unblocking already-written design, not re-deriving it.
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -214,11 +214,14 @@ require it for the common case.
|
||||
`/schema` at build time.
|
||||
|
||||
4. **`subscribe` (SSE) is the streaming projection for the gateway.**
|
||||
Over `h2`/`http/1.1`, subscriptions are SSE. Over WebTransport
|
||||
(`h3`), subscriptions project onto WebTransport streams directly
|
||||
(ADR-038) — the gateway's `/subscribe` is the `h2`/`http/1.1` path;
|
||||
the WebTransport path is the native call-protocol session
|
||||
(`webtransport.md`).
|
||||
Over `h2`/`http/1.1`, subscriptions are SSE. Over WebSocket (the v1
|
||||
browser bidirectional path, ADR-044), subscriptions project onto the
|
||||
WS connection directly as binary messages — the gateway's `/subscribe`
|
||||
is the `h2`/`http/1.1` SSE path; the WebSocket path is the native
|
||||
call-protocol session (`http-server.md` §"WebSocket browser path").
|
||||
WebTransport (`h3`, deferred per ADR-044) would project onto
|
||||
WebTransport streams; the deferred design is at
|
||||
`webtransport.md`.
|
||||
|
||||
## References
|
||||
|
||||
@@ -232,9 +235,11 @@ require it for the common case.
|
||||
- [ADR-036](036-http-to-call-operation-mapping.md) — the SSE projection
|
||||
for subscriptions over `h2`/`http/1.1` (the gateway's `/subscribe`
|
||||
endpoint uses the same SSE framing)
|
||||
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — the
|
||||
WebTransport streaming path (the gateway's `/subscribe` is the
|
||||
`h2`/`http/1.1` path; WebTransport is native)
|
||||
- [ADR-044](044-defer-webtransport-browsers-use-websocket.md) —
|
||||
WebSocket is the v1 browser bidirectional path; `h3`/WebTransport
|
||||
deferred (the gateway's `/subscribe` is the `h2`/`http/1.1` SSE path;
|
||||
the WS path is the native call-protocol session). ADR-038 is
|
||||
superseded by ADR-044.
|
||||
- [ADR-041](041-mcp-tool-gateway-pattern.md) — the sibling gateway
|
||||
pattern for `to_mcp` (4 tools; `subscribe` excluded because MCP tool
|
||||
calls are request/response)
|
||||
|
||||
@@ -2,7 +2,28 @@
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
**Proposed — implementation deferred per [ADR-044](044-defer-webtransport-browsers-use-websocket.md).**
|
||||
|
||||
This ADR's decision is correct and is not superseded. It revives unchanged
|
||||
when WebTransport revives, **with two transfers to WebSocket that apply
|
||||
during the deferment**:
|
||||
|
||||
- **§2 (call-protocol bidirectionality) transfers to WebSocket unchanged.**
|
||||
WebSocket is full-duplex; the call protocol's bidirectionality applies over
|
||||
a WS connection exactly as §2 describes for WebTransport. The browser case
|
||||
where the client registers no ops remains a use-case scoping, not an
|
||||
architectural limitation.
|
||||
- **§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 (ADR-044 §5); the
|
||||
connection-local Layer 2 overlay applies. The pattern is transport-agnostic.
|
||||
|
||||
What does **not** transfer to WebSocket is §4 (the non-call-ALPN substrate
|
||||
mechanism / the ALPN-stream-proxy, ADR-040) and §5's WebTransport-specific
|
||||
framing. Those require WebTransport's stream model and revive with it.
|
||||
ADR-044 §3 states the transfer explicitly; ADR-044 §5 states the
|
||||
"browser is not a peer" rationale (addressability vs. bidirectionality)
|
||||
that this ADR's §3 relies on but does not argue.
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
# ADR-044: Defer h3/WebTransport; Browsers Use WebSocket
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (supersedes ADR-038; parks ADR-040, ADR-043)
|
||||
|
||||
## Context
|
||||
|
||||
ADR-038 brought `h3`/WebTransport into scope as a first-class HTTP transport,
|
||||
framed against the "two-way door as deferral" anti-pattern (ADR-009 §"What
|
||||
this framework is NOT"). ADR-040 (the ALPN-stream-proxy) and 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 ADR-038 was written to correct. ADR-038 correctly rejected *deferral-
|
||||
as-hedging*; the present decision is *deferral-as-scoping*, which ADR-009
|
||||
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:
|
||||
|
||||
### 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**. ADR-043 §2 establishes that the call
|
||||
protocol's bidirectionality applies unchanged over any bidirectional stream —
|
||||
the `Dispatcher` is stream-agnostic (ADR-012). 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 a WebSocket binary message boundary cleanly (an
|
||||
`EventEnvelope` is a self-delimited JSON object; one frame = one WS binary
|
||||
message). 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.
|
||||
|
||||
What WebTransport gives *over* WebSocket — native multiplexed bidirectional
|
||||
streams, datagrams, the "carry any ALPN as a stream" substrate framing
|
||||
(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 (ADR-012); it does not need
|
||||
WebTransport's per-stream multiplexing. The substrate/proxy framing (ADR-040,
|
||||
ADR-043) is the thing that *does* benefit from WebTransport's stream model —
|
||||
and that use case is the speculative one (see Finding 3).
|
||||
|
||||
### Finding 2 — WebTransport is a draft standard on an experimental dependency stack
|
||||
|
||||
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 `alknet-http`'s first release. The `h3`
|
||||
feature gate (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.
|
||||
|
||||
### Finding 3 — the ALPN-stream-proxy is speculative; the call protocol is not
|
||||
|
||||
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 `alknet-http` (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 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.
|
||||
|
||||
### The iroh precedent
|
||||
|
||||
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.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Defer `h3`/WebTransport. Browsers reach the call protocol over WebSocket.
|
||||
|
||||
The `h3` ALPN, the `h3` feature gate, and the WebTransport dependency stack
|
||||
are **deferred** — not implemented in the initial `alknet-http` release. 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 call-protocol `EventEnvelope` frames as binary WebSocket
|
||||
messages. The browser is a bidirectional call-protocol client over this
|
||||
connection, using the same `Dispatcher` and `PendingRequestMap` as the
|
||||
`alknet/call` QUIC path (ADR-012 — stream-agnostic correlation; a WS message
|
||||
stream is just another `BiStream`-satisfying transport, extending ADR-012's
|
||||
stream-agnostic claim from QUIC bidirectional streams to any framed
|
||||
full-duplex byte channel).
|
||||
|
||||
This is a **scope** decision, not a hedging deferral (ADR-009 §"What this
|
||||
framework is NOT"). The reversal trigger is concrete: **a real deployment that
|
||||
needs the ALPN-stream-proxy (a browser running a WASM SSH/SFTP/git client to
|
||||
reach a non-call ALPN)**. When that use case arrives, ADR-038 / ADR-040 /
|
||||
ADR-043 revive as the design — they are not wrong, they are not-now. No
|
||||
"v1/later/when-it-arrives" hedging language attaches; the condition is stated
|
||||
as a concrete trigger.
|
||||
|
||||
### 2. ADR-038 is superseded by this ADR.
|
||||
|
||||
ADR-038's core decision — that `h3` is in scope, not deferred — is reversed
|
||||
by this ADR. 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. ADR-038 is marked Superseded.
|
||||
|
||||
### 3. ADR-040 and ADR-043 are parked, not superseded.
|
||||
|
||||
ADR-040 (the ALPN-stream-proxy) and ADR-043 (the bidirectional-substrate
|
||||
reframing) are **not superseded** — their decisions are correct, and they
|
||||
revive unchanged when WebTransport revives. They are marked Proposed with an
|
||||
amendment noting implementation is deferred per this ADR. Two specific
|
||||
transfers apply during the deferment:
|
||||
|
||||
- **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 ADR-043 §2
|
||||
describes for WebTransport. The browser case where the client registers
|
||||
no ops remains a use-case scoping, not an architectural limitation.
|
||||
- **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. The pattern is
|
||||
transport-agnostic.
|
||||
|
||||
What does *not* transfer to WebSocket is ADR-040 (the ALPN-stream-proxy) and
|
||||
ADR-043 §4 (the non-call-ALPN substrate mechanism). Those require
|
||||
WebTransport's stream model and revive with it. 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
|
||||
motivates 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.
|
||||
|
||||
`alknet-http`'s browser-reachable surface becomes:
|
||||
|
||||
| Transport | Direction | Use case |
|
||||
|-----------|-----------|----------|
|
||||
| `http/1.1`, `h2` | one-directional (client→server) | HTTP clients (curl, axios, `fetch` for request/response); SSE for subscription streaming (ADR-036) |
|
||||
| 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 ADR-043 §5 assigned to WebTransport). The
|
||||
one-directional projection that ADR-043 §5 names for HTTP/1.1+HTTP/2 stands
|
||||
unchanged.
|
||||
|
||||
### 5. Browsers over WebSocket are not alknet peers — the rationale, stated.
|
||||
|
||||
ADR-034 §4 established that a browser over WebTransport is not an alknet peer
|
||||
(no `PeerId`, no `PeerCompositeEnv` entry). The same applies to a browser over
|
||||
WebSocket, and the rationale — which ADR-034 §4 states as a closure without
|
||||
the supporting argument — is worth making explicit because it is the
|
||||
load-bearing distinction:
|
||||
|
||||
**"Peer" in alknet 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 (ADR-043 §3 / ADR-034 §2) 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 alknet 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 (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 (ADR-024, ADR-043 §3) is what
|
||||
makes the former possible without requiring the latter.
|
||||
|
||||
This rationale applies transport-agnostically — to WebSocket, to WebTransport
|
||||
when it revives, and to any future browser transport. ADR-034 §4 is amended
|
||||
by reference to this section.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- `alknet-http`'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 `alknet-http` unblocks (SSH, git, SFTP) are not
|
||||
blocked on WebTransport or the proxy. They expose their ALPNs natively over
|
||||
QUIC; browser reachability for them is a future WebTransport feature.
|
||||
- Forward momentum is preserved: the `h3` handler, the feature gate, the
|
||||
`wtransport`/hyperium decision, and the ALPN-stream-proxy are all real
|
||||
design work that is already done (ADR-038, ADR-040, ADR-043,
|
||||
`webtransport.md`). Reviving them is unblocking already-written specs, not
|
||||
designing from scratch.
|
||||
|
||||
**Negative:**
|
||||
- ADR-038, ADR-040, and ADR-043 are not implemented in the initial release.
|
||||
Their design work is preserved (the ADRs and `webtransport.md` stay in the
|
||||
record), but a reader must cross-reference this ADR to know they are
|
||||
parked. The `webtransport.md` spec is marked `deferred` with a header note.
|
||||
- The ALPN-stream-proxy (ADR-040) is not available in v1. A browser cannot
|
||||
reach SSH/SFTP/git ALPNs in the initial release — it can reach the call
|
||||
protocol over WebSocket, but not the non-call ALPNs. This is the
|
||||
speculative use case whose deferral this ADR commits; the reversal trigger
|
||||
is a real deployment needing it.
|
||||
- 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 — ADR-012), but it means a future migration to
|
||||
WebTransport would be a genuine upgrade, not a no-op. The migration path
|
||||
is the spec that already exists (`webtransport.md`).
|
||||
- ADR-043's "WebTransport restores bidirectionality" framing (§5) becomes
|
||||
"WebSocket restores bidirectionality" for v1. The framing transfer is clean
|
||||
(§3 above), but the prose in `http-server.md` and the ADRs must reflect it.
|
||||
|
||||
## Reversal
|
||||
|
||||
This decision reverses when a concrete deployment needs 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:
|
||||
|
||||
1. The research spike deferred here (verify the hyperium stack's server-side
|
||||
WebTransport API and the axum-bridge feasibility — see §"Research note"
|
||||
in `webtransport.md`) is run.
|
||||
2. 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.
|
||||
|
||||
The reversal is a one-way door at the *crate surface* (the `h3` feature gate
|
||||
becomes part of the published interface) but a two-way door at the
|
||||
*architecture* (the `webtransport.md` design already exists; reviving it is
|
||||
implementation work, not redesign). The `webtransport.md` spec is kept intact
|
||||
and marked `deferred` so the revival is unblocking, not re-deriving.
|
||||
|
||||
## Research note (for revival)
|
||||
|
||||
A note for the revival: `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 is that `alknet-http`'s `h3` handler must route HTTP/3 requests
|
||||
through the same axum `Router` as `h2`/`http/1.1` (ADR-036), 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 is **not** run now (WebTransport is
|
||||
deferred); it is recorded here so the revival does not re-derive the question
|
||||
from scratch. See `webtransport.md` §"Research note" for the cross-reference.
|
||||
|
||||
## Assumptions
|
||||
|
||||
1. **The call protocol's `EventEnvelope` framing fits a WebSocket binary
|
||||
message boundary cleanly.** An `EventEnvelope` is a self-delimited JSON
|
||||
object; one envelope per WS binary message. No streaming deserializer
|
||||
across message boundaries is needed. This is verified by implementation
|
||||
when the WS browser path is built, not by a separate research spike — the
|
||||
call protocol spec (`call-protocol.md`) and the EventEnvelope shape
|
||||
already make this property clear, and WebSocket binary messages are a
|
||||
standard byte-framed transport.
|
||||
|
||||
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 (ADR-043 §3, 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; 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 whose deferral this ADR commits.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-009](009-one-way-door-decision-framework.md) §"What this framework is
|
||||
NOT" — the anti-pattern ADR-038 was written to correct; this ADR relies on
|
||||
ADR-009's 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)
|
||||
- [ADR-038](038-http3-and-webtransport-as-first-class.md) — **superseded by
|
||||
this ADR.** Its correction of the two-way-door-as-deferral anti-pattern
|
||||
stands; its specific decision (h3 in scope now) is reversed.
|
||||
- [ADR-040](040-webtransport-alpn-stream-proxy.md) — **parked, not
|
||||
superseded.** Revives unchanged when WebTransport revives. The proxy is
|
||||
the speculative use case whose deferral is this ADR's reversal trigger.
|
||||
- [ADR-043](043-webtransport-bidirectional-alpn-substrate.md) — **parked, not
|
||||
superseded.** §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.
|
||||
- [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) §4 — browsers are
|
||||
not alknet peers; this ADR's §5 states the rationale (addressability vs.
|
||||
bidirectionality) that ADR-034 §4 closes without arguing. ADR-034 §4 is
|
||||
amended by reference to this ADR's §5.
|
||||
- [ADR-012](012-call-protocol-stream-model.md) — stream-agnostic correlation;
|
||||
a WebSocket message stream is another `BiStream`-satisfying transport. The
|
||||
call protocol multiplexes by request ID, not by stream.
|
||||
- [ADR-036](036-http-to-call-operation-mapping.md) — the HTTP-to-call
|
||||
mapping; the WebSocket browser path layers on top of the same axum
|
||||
`Router` and `OperationRegistry::invoke()` dispatch.
|
||||
- `crates/http/webtransport.md` — the deferred spec; marked `deferred` with
|
||||
a header note pointing here. Kept intact for revival.
|
||||
- `crates/http/http-server.md` — gains a "WebSocket browser path" section
|
||||
(the v1 browser bidirectional path) and the "browser is not a peer"
|
||||
rationale (this ADR's §5, transported to the spec that now carries the
|
||||
browser path).
|
||||
Reference in New Issue
Block a user