Full bidirectional gRPC in the browser — real HTTP/2, REST, and WebSockets, on the same port as native gRPC.
grpc-webnext brings the complete gRPC experience to the browser — all four call types, deadlines, metadata, trailers, and cancellation — with no translation on the default path. The browser speaks real HTTP/2 (trailers, flow control, native multiplexing) tunneled over a WebSocket by h2ts, straight into an unmodified gRPC server. Want plaintext instead? Flip one switch for JSON over Fetch + WebSocket, or annotate a method for plain REST. One endpoint serves browsers, REST clients, and native gRPC clients — all on the same port.
import { makePromiseClient } from "@grpc-webnext/client";
import { GreeterDefinition } from "./gen/greeter.js";
const rpc = makePromiseClient(GreeterDefinition, { baseUrl: "https://api.example.com" });
const reply = await rpc.sayHello({ name: "world" }); // unary
for await (const tick of rpc.countdown({ from: 3 })) … // server-stream
for await (const msg of rpc.chat(source, { signal })) … // bidi, cancel via AbortSignal- Real gRPC, end to end. The default binary path is real HTTP/2 over h2ts into an unmodified server — no envoy-style translation, no lossy shim. Trailers, flow control, and stream multiplexing are the genuine article.
- Same port as native gRPC.
content-typedisambiguatesapplication/grpcfrom grpc-webnext; native gRPC clients pass through untouched. One listener, every audience. - Two codecs. Efficient binary protobuf, or plaintext JSON you can read in the browser's Network tab. Pick per client.
- Two streaming transports. h2ts (one WebSocket, many multiplexed streams) or a
custom
Frameprotocol (one WebSocket per stream) — configurable per client. - Raw REST, built in. Annotate methods with
google.api.http(the grpc-gateway / Envoy standard) to expose real HTTP verbs and REST URLs with JSON bodies. - WebSockets in JSON or binary. Stream over a debuggable JSON WebSocket or a compact binary one — same semantics, your choice.
- Full gRPC semantics. Unary · server-stream · client-stream · bidi, plus
grpc-timeoutdeadlines, metadata (ASCII +-bin), canonical status codes, and cancellation. - In-process or proxy. Wrap a tonic
Routesfor a zero-hop native endpoint, or run the schema-agnostic proxy in front of any gRPC server, in any language. - Standard auth. No bespoke hooks — authorization is a per-RPC gRPC interceptor
(in-process) or your mesh's
ext_authz(proxy), uniform across every transport. - Polyglot, drift-proof. One wire format, implemented per language, held to a language-neutral conformance suite run over the real wire.
- Familiar client API. Callback/EventEmitter and promise/async-iterable flavors,
modeled on
@grpc/grpc-jsand Connect.
Browser / Node clients (TypeScript ✅ · Rust-WASM ⬜)
│
│ proto (default) json / proto-ws
│ real HTTP/2 ⤵ h2ts custom Frame protocol ⤵
▼ (Fetch unary · WebSocket streams)
┌───────────────────────────────────────────────────────────┐
│ grpc-webnext endpoint — one port │
│ in-process server: Rust ✅ · Go ✅ · Node ⬜ │
│ or standalone proxy (Rust) — front any gRPC upstream │
└───────────────────────────────────────────────────────────┘
│ native application/grpc (same port, byte-for-byte passthrough)
▼
Any gRPC server — tonic · grpc-go · grpc-java · …
Two worlds share one endpoint:
- Binary (default) → real gRPC over h2ts. The browser runs a real HTTP/2 stack in TypeScript, tunneled over a WebSocket by h2ts; the server is unmodified tonic behind an h2ts gateway. No translation — trailers, multiplexing, and flow control are native.
- JSON (and binary
streaming: "ws") → the customFrameprotocol. Unary rides Fetch (the response body is[len│message][len│trailer], since browsers can't read HTTP trailers); streaming is one WebSocket per stream, one protobufFrameper message (Subscribe·Message·HalfClose·Trailer·Reset·Header). Plaintext JSON keeps it debuggable in the browser.
Transport is a per-client config { codec, unary, streaming }:
| Client config | Unary | Streaming | On the wire |
|---|---|---|---|
{ codec: "proto" } (default) |
h2ts | h2ts | Real HTTP/2 over one WebSocket — multiplexed, unmodified gRPC |
{ codec: "proto", unary: "fetch" } |
Fetch | h2ts | Fetch unary + multiplexed HTTP/2 streams |
{ codec: "proto", streaming: "ws" } |
Fetch | one WS / stream | Custom Frame protocol — binary |
{ codec: "json" } |
Fetch | one WS / stream | Custom Frame protocol — plaintext JSON |
| REST (server-annotated) | any HTTP verb | — | Plain JSON on /v1/… URLs — grpc-gateway-style |
The wire format is defined once, in proto/grpc_webnext.proto and
two normative documents — spec/PROTOCOL.md for the custom Frame path
(Fetch unary + one WebSocket per stream) and spec/PROTOCOL_H2TS.md
for the binary default, which is real gRPC over a tunnel and so mostly delegates to the
HTTP/2 and gRPC specs. Every implementation is held to both by the
conformance suite. See spec/COMPATIBILITY.md for
per-transport gRPC-semantics fidelity.
Run the demo — starts the native Rust server and drives it from the TypeScript client:
cd node/packages/client && npm install && npm run demoUse the client — npm install @grpc-webnext/client; two flavors share one transport (add
codec: "json" for plaintext):
import { makeClient, makePromiseClient } from "@grpc-webnext/client";
import { GreeterDefinition } from "./gen/greeter.js"; // ts-proto generic definitions
// Callback / EventEmitter — mirrors @grpc/grpc-js
const cb = makeClient(GreeterDefinition, { baseUrl: "https://api.example.com" });
cb.sayHello({ name: "world" }, (err, reply) => console.log(reply.message));
// Promise / async-iterable — mirrors Connect / nice-grpc
const rpc = makePromiseClient(GreeterDefinition, { baseUrl: "https://api.example.com" });
const reply = await rpc.sayHello({ name: "world" }); // unary → Promise
for await (const t of rpc.countdown({ from: 3 })) log(t); // server-stream → AsyncIterable
const sum = await rpc.concat(source); // client-stream → Promise
for await (const m of rpc.chat(source, { signal })) log(m); // bidi, cancel via AbortSignalWrap a tonic server in-process (serves grpc-webnext and native gRPC on one port):
use grpc_webnext::{bind_and_serve_in_process, ServerConfig};
let routes = tonic::service::Routes::new(GreeterServer::new(svc));
let (addr, handle) = bind_and_serve_in_process(routes, ServerConfig::default()).await?;Front an existing gRPC server with the proxy (language-agnostic, no .proto required):
cd rust
UPSTREAM=http://localhost:50051 LISTEN=127.0.0.1:8080 cargo run -p grpc-webnext-proxy| Component | Location | State |
|---|---|---|
| Wire protocol + normative spec | proto/ · spec/PROTOCOL.md · spec/PROTOCOL_H2TS.md |
✅ the contract |
| Rust server + proxy · crates.io | rust/crates/grpc-webnext |
✅ h2ts, custom Frame, +json, REST, deadlines, cancel, size limits, native same-port |
| TypeScript client (browser + Node) · npm | node/packages/client |
✅ h2ts + Fetch + WebSocket, typed codegen, callback + promise APIs |
| Conformance suite | conformance/ |
✅ language-neutral cases × Rust and Go servers × TS driver, run over the real wire |
| Go in-process server | go/webnext |
✅ h2ts, custom Frame, +json, REST, deadlines, cancel, size limits, native same-port |
| Node in-process server | node/packages/server |
⬜ skeleton |
| Rust client (WASM / frontend) · crates.io | rust/crates/grpc-webnext-client |
✅ h2ts, all four cardinalities, deadlines, reconnect + connectivity — no hyper/tokio, and tonic-free by default, with an optional feature that runs tonic's own generated stubs over the tunnel |
Pre-1.0. The Rust server/proxy, the Go server, and the TypeScript client are covered by the conformance suite, which runs every case against both server implementations; the Node server is the remaining polyglot milestone. See
doc/BACKLOG.md— including a plan to terminate grpc-webnext inside stock Envoy via a Rust dynamic-module filter, no sidecar.
Organized by language ecosystem — each toolchain owns its subtree — with the cross-language contract at the root.
proto/ wire envelope (Frame, Metadatum, …) — source of truth
spec/ PROTOCOL.md + PROTOCOL_H2TS.md (both normative) + COMPATIBILITY.md
conformance/ language-neutral conformance suite (proto, cases, driver)
doc/ design notes (STATUS, BACKLOG, UNIFICATION, H2TS_INTEGRATION,
GO_SERVER, HTTPRULE_GAPS)
rust/ Cargo workspace
crates/grpc-webnext/ server library + proxy binary (grpc-webnext-proxy)
examples/ Greeter service, end-to-end demo, conformance server
node/ npm workspace
packages/client/ @grpc-webnext/client — Fetch + WebSocket + h2ts transports
packages/server/ @grpc-webnext/server — in-process (skeleton)
go/ Go module (github.com/grpc-webnext/grpc-webnext/go)
webnext/ in-process server — Fetch + WebSocket + h2ts + native gRPC
examples/ Greeter service, conformance server
cd rust && cargo test --workspace # Rust: server + proxy
cd rust && cargo clippy --workspace --all-targets
cd go && go test -race ./... # Go: wire unit tests + end-to-end over real sockets
cd node && npm ci && npm test # TypeScript: codec, e2e, and the conformance matrixThe conformance matrix runs every case against both server implementations, so the
TypeScript suite needs a Rust and a Go toolchain — it builds and spawns both servers.
Servers print LISTENING http://<addr> when ready.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.