ADR 0022 — gRPC over ConnectRPC
Accepted · 2026-06-17
Supersedes the gRPC-transport decision (§3) of ADR 0003.
Context
Section titled “Context”ADR 0003 shipped the first streaming gRPC: Connect-Web spoke HTTP/2 to the upstream directly for server-streaming, the Worker stayed in the unary path, and the desktop data plane ran on @grpc/grpc-js. Client-streaming and bidi were stubbed, and the streaming client hand-encoded Connect envelopes.
That arrangement accumulated real problems on desktop, all rooted in grpc-js:
- TLS knobs didn’t work.
grpc-jshas no equivalent ofrejectUnauthorized: false, so the user’s “verify SSL off” setting was silently ignored, and an encrypted client key + passphrase wasn’t handled natively — a correctness gap for a tool that routinely hits self-signed staging and mTLS endpoints. - SSRF IP-pinning was awkward. The other streaming handlers pin a pre-validated IP at connect time (ADR 0006);
grpc-jsdidn’t fit thatlookup-based pattern cleanly. - Two descriptor/encoding paths. Web used runtime descriptors + manual Connect envelopes; desktop used
grpc-js. The same call was assembled two ways, and only one could do client/bidi streaming.
Decision
Section titled “Decision”Adopt ConnectRPC as the single gRPC stack across every backend, driven by one backend-agnostic runtime descriptor registry.
- One descriptor source —
shared/protocol/grpc-registry.tsbuilds@bufbuild/protobufruntime descriptors from reflection or an uploaded.proto/ FileDescriptorSet (no codegen); both backends call against it. - Web — the shared proxy speaks the Connect protocol (
Connect-Protocol-Version: 1) through the Worker for unary + server-streaming; browser-direct streaming usesconnect-web. - Desktop —
connect-node’screateGrpcTransport(native gRPC over real HTTP/2) with an automaticcreateConnectTransportfallback for Connect-only servers.grpc-jsis removed from the data plane. - TLS lives in Node’s
http2/tlsoptions, sorejectUnauthorized: false, a custom CA, and an encrypted client cert + passphrase all take effect. - SSRF pinning is a
nodeOptions.lookupreturning the already-validated IP; the authority / SNI stay on the hostname, so certificate validation is unchanged. - Client-streaming and bidi now work on desktop (real h2). The web target keeps unary + server-streaming, since browser
fetchcan’t stream a request body.
Consequences
Section titled “Consequences”Positive
- “Verify SSL off”, custom CAs, and encrypted client certificates actually take effect for gRPC on desktop.
- One descriptor registry and one protocol family across web and desktop; the hand-rolled envelope path is gone.
- Client/bidi streaming is implemented on desktop, closing the ADR 0003 stub.
- gRPC SSRF pinning is unified with the other streaming handlers’
lookuppattern.
Negative
connect,connect-node, andconnect-webare added dependencies.- Web and desktop differ in streaming capability (web can’t do client/bidi over
fetch) — recorded in the capability matrix, not hidden. - A desktop call can land on either native gRPC or the Connect fallback depending on the server; the negotiated path is surfaced in the response, but it’s one more thing to reason about.
Alternatives considered
Section titled “Alternatives considered”- Keep
grpc-js. Rejected — no TLS-verification knob, no native encrypted-key handling, an awkward fit forlookup-based SSRF pinning, and a second descriptor/encoding path forever. - Generated
bufbuildclients. Rejected — Restura is built on runtime proto reflection; codegen breaks the dynamic-descriptor model. node:http2directly. Rejected — reimplements framing, pooling, and ALPN thatconnect-nodealready provides.
Related
Section titled “Related”- gRPC protocol — the user-facing feature this transport backs.
- ADR 0001 — Shared Protocol Layer — the single-core pattern the descriptor registry follows.
- ADR 0003 — Streaming and HTTP/2 — superseded in part by this ADR.
- ADR 0006 — Electron Connection Cleanup + Pre-flight DNS Guard — the
lookup-based SSRF pinning this transport now shares.