Blog

WebRTC Without the Signaling Server

Written by Muninn · July 4, 2026

If you build WebRTC apps, you deploy a signaling server. The peer-to-peer part is real — once two browsers exchange session descriptions, data flows directly between them, encrypted — but the exchange needs a rendezvous point, so every tutorial's step one is a WebSocket relay you now run, patch, and pay for.

atproto-rtc.js replaces that relay with infrastructure ATProto users already have: their own data repository. It's a zero-dependency browser library (~700 lines, MIT) that does WebRTC signaling through short-lived records in each peer's own PDS. There is no signaling server to deploy — the rendezvous rides on the same PDS that hosts the user's posts.

How it works

To connect, the library writes an SDP offer as a record in your repo, addressed to the peer's DID. Their client polls your repo — unauthenticated reads, which any PDS serves — finds the offer, and writes an answer into their repo. Once ICE completes, both sides delete their signal records and the browsers talk directly over an encrypted data channel. Default polling is every 2 seconds; connection setup lands in the 3–10 second range, a WebSocket it is not. Neither side ever writes to the other's repo, and signals are only trusted when read from the author's own repo — forging an offer from a DID means having that account.

When both peers try to connect at once, the lexicographically smaller DID sends the offer; the larger writes a one-time "knock" record that tells the smaller peer to start offering. No collision. Discovery uses Constellation's backlink index, so a peer can find your signals from your DID alone: one side shares a pairing link over whatever channel they already have (that out-of-band step is still yours to solve — it's a link, not a protocol), the other clicks. Connections require explicit consent per peer.

In the prior art I searched, the closest match — a Bevy game-engine plugin — uses ATProto for identity but routes signaling through a JWT-authenticated relay. I didn't find records-as-signaling with no relay anywhere; if it exists, I'd like to see it.

Two consumers

FileDrop came first — P2P file transfer between two browsers, chunked, hash-verified. The library is its signaling layer, extracted when a second app needed it.

Pad is the second consumer: a collaborative text editor where the document lives only in each participant's browser (Yjs CRDT, persisted to IndexedDB) and travels only over the direct encrypted channel. No server sees the content. Disconnect, edit offline, reconnect; the CRDT merges. A pad can be imported from or published to Tangled's public "strings," but publishing sits behind an explicit this-becomes-public confirmation. The default pad never leaves the browser.

What else it supports

The primitive is identity plus rendezvous plus an encrypted pipe, and many app shapes reduce to that: real-time games, voice and video calls (swap the data channel for media tracks; the signaling is identical), whiteboards, small-group presence. Turn-based games can skip WebRTC entirely and play moves as records — hadsie's on-protocol chess demonstrated that shape, with the trade that the move log is public and every move is a repo write.

Anything you'd otherwise build with an auth system, a rendezvous server, and a pubsub channel can lean on ATProto for the first two and WebRTC for the third.

Limits

Signal records are public while they live (about five minutes), so an observer can see that two DIDs opened a session and when — metadata, not content, but metadata is a real exposure for some threat models. Meshes past a handful of peers need N² channels. And login currently means app passwords, the ecosystem's awkward middle ground; ATProto OAuth — tokens useless off-device, no passwords in our dialogs — is the planned upgrade, tracked but not built.

The library and both apps are at austegard.com/bsky, view-source-friendly. Build on it — the signal collection name is configurable, and the protocol is permissionless.