Saturday, October 31, 2026

WebRTC-Direct examples in every language: no signaling, plus TURN and STUN modes

The latest grpc-over-datachannels release ships WebRTC-Direct examples for every supported language — generated, like the runtimes themselves. A server listens on a UDP socket and prints a certhash multiaddr (/ip4/…/udp/…/webrtc-direct/certhash/f…); a client dials that address and gets a DTLS/SCTP data channel carrying plain gRPC frames. No signaling server, no SDP exchange.

Three ways to connect

Direct — the client dials the multiaddr straight over UDP. This is the LAN and public-host path, and the server prints everything a client needs on one MULTIADDR line.

Via TURN — a client behind a symmetric NAT allocates a relay on a TURN server and dials the same listener through it. TURN is purely a client-side tool here: the server never changes and never learns TURN exists. Set DIRECT_TURN=host:3478 (plus user/password) or pass --turn to the sidecar.

Full ICE with STUN — a Direct listener gathers no ICE candidates, so STUN belongs to the peer mode: the Go and Rust examples include peer-serve/peer-dial modes where GRPC_DC_STUN=stun.l.google.com:19302 enables server-reflexive candidate gathering, with SDP exchanged through files as a stand-in for any signaling channel. Browsers join through this same full-ICE path — a browser has no UDP sockets, so it can never dial a multiaddr directly.

Every language, one shape

JavaScript, Go, and Rust embed the WebRTC stack and run the examples natively. The other thirteen languages — Python, Ruby, PHP, Lua, C, C++, Dart, Java, Kotlin, Scala, Swift, C#, and Zig — reach WebRTC-Direct through their generated SidecarTransport plus the small Rust direct_sidecar bridge, so the language code is identical whether the path is direct or TURN. Each example has serve and dial modes, and the compose file in examples/direct wires servers and clients together with a shared /signal volume as the whole client→server handoff, coturn fixture included.

Try it

Everything lands in examples/direct in the upcoming v0.36.0 release (December 1): start the Node server, copy its multiaddr, and dial it from any language. The per-language catalog lives at browser-grpc/examples. Two honest limits, documented in the README: a NATted listener still needs a port forward, a public host, or the TURN dial — STUN alone cannot make an inbound UDP listener reachable — and browser + tunnel + TURN is not supported, because the quick tunnels are TCP-only.

Friday, October 30, 2026

v0.35.12: the event bus gets its own repository

Release v0.35.12 of grpc-over-datachannels is out, and its headline is organisational: the generated event bus now has a dedicated home at browser-grpc/eventbus, the same treatment the Kafka proxy got with browser-grpc/kafka-proxy.

What is in the repo

The schema (proto/events.proto, grpcdc.events.v1.EventBus), the complete dependency-free Python and JavaScript trees — broker, clients, demos, and tests that run as-is — and the generated EventBus stubs, brokers, demos, and tests for the other fourteen languages under gen/<lang>/, ready to drop into the main repo's runtime trees. An EVENTBUS.md guide covers the service surface: typed routing tags, containment filters, broker-assigned sequence numbers, and the git-backed log variant.

Run it

The demo ships as a container: docker run --rm ghcr.io/browser-grpc/eventbus-demo:latest starts a broker, subscribes with a topic filter, and publishes three events so you can watch the filter admit two of them. The repo's CI builds the test image and runs the Python and JavaScript suites plus both demos on every push — it is green on the release commit.

Everything else moved too

As usual the release is published across GitHub, npm (grpc-dc), PyPI, NuGet (GrpcDc), Maven Central (io.github.browser-grpc:grpc-dc), and GHCR, all at 0.35.12. Background on the design is in A generated event bus; the main project remains browser-grpc/grpc-over-datachannels.

A generated event bus: publish/subscribe as a gRPC service in sixteen languages

Most systems eventually grow an event bus, and most event buses arrive as a heavyweight broker plus a client library per language. grpc-over-datachannels takes the generator's route instead: define the bus once as a gRPC service — grpcdc.events.v1.EventBus — and generate the broker and clients in all sixteen languages from that one schema.

The whole API is two calls

Publish takes routing tags, an opaque payload (typically your own serialized event proto), and a publisher id, and returns a broker-assigned sequence number. Subscribe takes a containment filter and returns a stream of events. An event carries its tags, payload, the broker-global sequence (1-based, in publish order), publisher id, and the broker's wall-clock timestamp.

Tag-value filtering

Tags are typed values — string, int64, double, bool, or any enum carried by number so it survives peers that do not know the enum. A subscription filter is a list of (tag, value) pairs, and an event is delivered when its tag list contains every pair in the filter; an empty filter receives everything, and the same tag may appear several times when an event must carry several values. There is no query language to learn and no stringly-typed topic hierarchy to govern: matching is set containment, evaluated the same way in every generated implementation.

Why generated matters here

Because the bus is just a .proto, publishers and subscribers can be browser tabs, native services, or mixed pairs over a DataChannel, and the broker itself has a generated implementation per language — including a git-backed event log variant for durable, inspectable history. Sequence numbers make ordering claims testable, and the same generated BDD scenarios that grade the runtimes grade the bus. If your architecture needs events, you already have the client: it was generated with your other stubs.

Update (v0.35.12): the event bus now has a dedicated repository — browser-grpc/eventbus — with the schema, the full Python and JavaScript trees, generated sources for the other fourteen languages, and CI. Run the demo with docker run --rm ghcr.io/browser-grpc/eventbus-demo:latest.

One open port for every peer: PeerMux and single-socket WebRTC serving

Running a WebRTC server usually means socket sprawl: a listener per peer, or a framework that owns your sockets for you. grpc-over-datachannels takes the opposite approach with PeerMux: many signaled peers hosted on one bound UDP socket — the single-port topology popularised by large conferencing systems — implemented in the Rust, Go, and JavaScript (Node) stacks.

Routing packets without addresses

A mux cannot route by source address: peers sit behind NATs, addresses get reused, and two muxes may peer with each other. So PeerMux routes by protocol contents instead. STUN packets route by ICE ufrag. The first DTLS flight routes by a pair-hint extension. Every later DTLS record routes by its RFC 9146 connection id. Once the DataChannel is up, the gRPC frames never see the socket at all — the mux is purely a packet demultiplexer under the stack.

What falls out of the design

A server needs exactly one open port for all its peer connections, which simplifies firewalls, containers, and home hosting. TURN still works on muxed peers (the allocation's UDP leg rides a dedicated per-peer socket). And because routing never depends on who sent a packet from where, two muxes can peer with each other directly.

Verified cross-stack

One generated scenario tests all three stacks, and the cross-stack matrix pairs mux against mux across implementations — Rust serving Go, Go serving JavaScript, and so on — so the routing rules are graded against independent code, not just against the implementation that proposed them. If you are embedding WebRTC serving into a native process, one port is all you have to give it.

WebRTC with no operating system: the embedded profile

WebRTC is usually something that happens on phones, browsers, and servers — places with an operating system, a network stack, and memory to spare. But a gRPC-speaking sensor or controller should not need Linux just to make an encrypted call. The WebRTC stack under grpc-over-datachannels is built no_std for exactly that reason.

Traits instead of an OS

The Rust stack cross-builds for Cortex-M. Everything an OS would normally provide is injected through platform traits: a clock, an entropy source, and the datagram transport. On top of those traits sits an in-crate network stack — Ethernet framing, ARP, IPv4/IPv6, UDP, and DHCP — so the device does its own networking from the wire up: plug in Ethernet, get an address, run ICE/DTLS/SCTP, open a DataChannel, speak gRPC.

Proving it without hardware on your desk

The bring-up is demonstrated by a two-device example over a virtual cable (cargo run --example iot_cable in webrtc/rust): power-on to DataChannel echo between two stack instances with nothing between them but the simulated wire. That same frame protocol then carries generated gRPC stubs — the small C99 and Lua runtimes target constrained hosts too, and every generated runtime is dependency-free, which matters when there is no package manager to lean on.

Why bother

Because the alternative for embedded RPC is usually a bespoke protocol you now own forever. A generated stub, a standard wire format, and interoperability graded against browsers and stock gRPC implementations give an embedded target the same contract as every other peer in the system — including calling, and being called by, a browser tab.

One data channel, many calls: the grpc-dc frame protocol

gRPC normally rides on HTTP/2: one TCP connection, many streams, HPACK headers, flow-control windows. WebRTC DataChannels already give us a reliable, ordered, encrypted message pipe — so grpc-over-datachannels replaces the HTTP/2 layer with a small frame protocol designed for that pipe. This post is a tour of the wire format, specified in SPEC.md in the repository.

One message, one frame

DataChannels preserve message boundaries, so every binary message is exactly one frame: a type byte, a 32-bit big-endian stream id, and a payload. The frame types are deliberately few. HEADERS opens a call and carries a small JSON document — the method path and metadata — instead of an HTTP header block. DATA carries exactly one serialized protobuf message, untouched. HALF_CLOSE says the client will send no more, mirroring gRPC semantics. TRAILERS ends the call with status, and RST aborts from either side.

Stream lifecycle

The initiating peer allocates odd stream ids (1, 3, 5, …), the other side even ones, so both peers can open calls without coordination. A full-duplex call is: HEADERS from the client, HEADERS back with initial metadata, any number of DATA frames, HALF_CLOSE, more DATA, then TRAILERS. A unary call is just the degenerate case of one DATA frame each way — there is no separate unary fast path to keep in sync across sixteen runtimes.

The details that matter

Deadlines travel as the familiar grpc-timeout metadata key, recomputed at each forwarding hop. Unknown frame types are ignored, which gives the protocol forward compatibility without a version negotiation. Malformed frames, unknown stream ids, and calls to unregistered methods fail closed with an error status. And because a data channel is a stream of messages rather than bytes, backpressure is handled per stream at the frame level rather than by parsing a byte stream.

The full specification, including tracing propagation and the deliberate divergences from gRPC-over-HTTP/2, lives in the grpc-over-datachannels repository. Try the frames live in the in-browser generator.

Rebuilding WebRTC from scratch: ICE, DTLS, SCTP with zero dependencies

Every language runtime in grpc-over-datachannels is generated and dependency-free — but a gRPC client in Go or Rust still needs something underneath that speaks WebRTC to a browser. Native WebRTC libraries are large, stateful, and different in every language. So the project reimplements the WebRTC stack itself: ICE for connectivity, DTLS for encryption, SCTP for the data-channel transport, and DCEP for channel setup. This post explains the profile we implemented and the rules that kept it tractable.

A profile, not the whole RFC stack

WebRTC is a family of specifications with many optional corners. The reimplementation targets a profile: the subset real browsers actually negotiate, verified continuously against Chromium and Firefox (driven headlessly through Playwright) rather than against our reading of the RFCs alone. Where browsers diverge from the documents, the browsers win, and the divergence is written down in the spec.

What is in the stack

STUN/TURN for NAT traversal (with per-language tests against public STUN servers in CI), ICE with host/server-reflexive/relay candidates, DTLS 1.2 handshakes, SCTP with ordered-reliable and lossy/unordered channel modes, and a single-socket multiplexer (PeerMux) that hosts many peers on one bound UDP port. WebRTC-Direct gives native peers a libp2p-style direct dial without SDP file shuffling. The Rust stack is no_std: platform traits inject clock, entropy, and datagrams, and an in-crate network stack provides Ethernet framing, ARP, IP, UDP, and DHCP, so the same code runs on embedded targets with no operating system.

Why zero dependencies is the point

A generated client you paste into a project should not drag in a dependency tree you have to audit. The stack builds from each language's standard library only, which is also what makes the same wire behaviour reproducible across the Go, JavaScript, and Rust stacks: they are checked against each other and against real browsers, not against a shared library's quirks. The design rules and verification strategy are in SPEC-WEBRTC.md in the repository.

Trust but verify: oracle-testing generated gRPC against Google's implementations

Code generation has a trust problem: if the generator and its tests share an assumption, the tests pass and the code is still wrong. grpc-over-datachannels generates its gRPC runtimes and most of its tests from one source of truth, which makes that failure mode easy to fall into. The defence is an oracle: stock, Google-maintained gRPC implementations that never saw our generator.

The bridge

A TCP-to-DataChannel proxy connects the two worlds. On one side it speaks ordinary HTTP/2 gRPC over TCP, exactly what grpc-java and grpc-go expect. On the other it speaks the grpc-dc frame protocol over a WebRTC DataChannel. A generated client can therefore call a stock server, and a stock client can call a generated server, with every frame crossing both wire formats on the way.

What gets graded

The interop matrix runs the standard gRPC test vocabulary — unary, server-streaming, client-streaming, bidirectional streaming, empty calls, large messages, status codes, metadata, and deadlines — across generated clients and servers in JavaScript, Go, Rust, Python, and the other generated languages, against the reference implementations as the source of truth. The same generated scenario table (the BDD suite) also runs natively per language and inside real Chromium, Firefox, and WebKit browsers via Playwright, so a behaviour has to be right in three independent venues before it counts.

Why this matters more than unit tests

Unit tests verify our code against our understanding. The oracle verifies our understanding against the ecosystem's. Every divergence the proxy runs surfaced — in framing, trailers, timeout formats, or stream teardown — was a place the specification in our heads differed from the one on the wire, found before a user could find it for us. If you generate protocol code, budget for an oracle: it is the only test suite that cannot agree with your mistakes.

Inside kafka-proxy: consumer groups, rebalances, and offset commits over gRPC

kafka-proxy exposes Apache Kafka as a generated gRPC service, grpcdc.kafka.v1.Kafka, so any grpc-over-datachannels client — including a browser tab — can produce and consume without a Kafka client library. The interesting engineering is not the produce path; it is consumer groups, where Kafka's protocol is a distributed-systems negotiation. This post walks through how the proxy holds that state.

Sessions live on the proxy

Kafka group membership is stateful: joins, syncs, heartbeats, offset commits, and generation ids that fence off zombie members. Browser and thin native clients should not each reimplement that, so producer and consumer-group sessions live on the proxy. A client opens a stream and subscribes; the proxy joins the group, distributes assignments, and heartbeats on the client's behalf, against both Apache Kafka (KRaft) and Redpanda.

Rebalances are the hard part

When group membership changes, the coordinator bumps the generation and in-flight commits from the old generation are rejected. The proxy treats that as weather, not failure: broker connections reconnect once after transport errors, offset commits retry inside the session window until the new generation accepts them, and members rejoin without tearing down the client's stream. The end-to-end suite (21 tests per broker) races commits against forced rebalances precisely because that interleaving is where offsets get lost.

Restart resume

The newest test replaces the entire proxy process mid-group: a new instance comes up, the group reforms, and consumption resumes exactly at the committed offset — no redelivery storm, no skipped records. That property is what makes the proxy deployable as a stateless-ish sidecar in front of a broker: run it as ghcr.io/browser-grpc/kafka-proxy, point it at Kafka or Redpanda, and generated clients in sixteen languages can treat Kafka as just another gRPC service.

grpc-dc everywhere: npm, PyPI, NuGet, Maven Central, GHCR, and Go modules

Generated code is only useful if it drops into the package manager you already use. With release v0.35.10, grpc-dc — gRPC over WebRTC DataChannels, with generated dependency-free runtimes — is published across the registries:

Every artifact is generated from the public repositories — browser-grpc/grpc-over-datachannels and browser-grpc/kafka-proxy — under Apache-2.0, and code generated from your own .proto files is yours without restriction. Public CI in both repos runs the trimmed exported test suites in generated Dockerfiles, so you can reproduce exactly what the registries received.

New here? Start with the live in-browser generator: paste a .proto, pick languages, download stubs and runtimes — no install. Documentation is at browser-grpc.github.io/grpc-over-datachannels. More launch notes, design posts, and release write-ups will follow on this blog.

Kafka over gRPC: a Rust Kafka proxy with generated clients for browser and native

Our second launch is kafka-proxy: a Rust proxy that exposes Apache Kafka through a generated gRPC service, grpcdc.kafka.v1.Kafka, so any generated grpc-over-datachannels client — browser or native — can produce to and consume from Kafka without a Kafka client library in that language.

The proxy terminates generated gRPC on one side and speaks the real Kafka protocol on the other, to both Apache Kafka (KRaft) and Redpanda. Producer sessions and consumer-group sessions live on the proxy side: clients open streams, publish records, subscribe, and commit offsets through gRPC calls, while the proxy handles broker connections, group membership, and rebalances. Broker connections reconnect after transport failures, and offset commits retry through generation churn, so a rebalance does not silently lose progress.

Coverage

The proxy is exercised by an extensive generated end-to-end suite — 21 tests per broker — covering produce/consume round trips, consumer groups, offset commits across rebalances, and resuming a group after the proxy itself restarts. The same suite runs against Apache Kafka and Redpanda in CI, in generated Dockerfiles that readers can run themselves.

Run it

Code, guide, and CI live at browser-grpc/kafka-proxy. A prebuilt container image is published as ghcr.io/browser-grpc/kafka-proxy, and the main project is at browser-grpc/grpc-over-datachannels. If you can call gRPC from your language — including from a browser tab — you can now reach Kafka through it.

For the internals — how producer and consumer-group sessions, rebalances, and offset commits are held on the proxy — see Inside kafka-proxy: consumer groups, rebalances, and offset commits over gRPC.

Introducing grpc-over-datachannels: gRPC over WebRTC DataChannels, without HTTP/2

Today we are launching grpc-over-datachannels: gRPC running over a single WebRTC RTCDataChannel instead of HTTP/2. Many concurrent calls — unary, server-streaming, client-streaming, and bidirectional streaming — are multiplexed onto one ordered, reliable data channel using a small frame protocol, with no HTTP/2 stack and no server endpoint to reach.

The runtimes are generated and dependency-free. From one source of truth, recursive metaprogramming generates the gRPC runtime and protoc plugins for Rust, JavaScript, Go, Python, Java, C#, C++, Swift, Kotlin, Dart, Zig, C99, Lua, Ruby, PHP, and Scala — for native clients and servers, and for browser clients (including WASM facades that wrap the JavaScript browser client). Generated tests verify the wire protocol against stock gRPC implementations through a TCP-to-DataChannel proxy, so behaviour is checked against the reference, not just against itself.

Try it in the browser

No install is needed to generate stubs for your own service. Open the live in-browser generator, paste or edit a .proto file, pick your languages, and download generated stubs plus the matching runtimes. Schema compilation and the actual code generators run entirely client-side in the page.

Get the code

Source, specification, quick start, and examples are on GitHub: browser-grpc/grpc-over-datachannels. Documentation lives at browser-grpc.github.io/grpc-over-datachannels. Current release: v0.36.0.

This blog is now the project log for grpc-over-datachannels. The older low-latency and systems posts below — on websockets, AF_XDP, kernel latency, and socket multiplexing — are the background this work grew out of, and they are staying. Next up: the Kafka proxy, and the package ecosystem.

Dig deeper

The wire format under all of this is specified in One data channel, many calls: the grpc-dc frame protocol. Underneath it sits our own WebRTC stack: Rebuilding WebRTC from scratch: ICE, DTLS, SCTP with zero dependencies. And the generated runtimes are graded against Google's implementations, not just themselves: Trust but verify: oracle-testing generated gRPC.