How Peer-to-Peer Sync Works Over WebRTC
WebRTC is best known for video calls, but the same machinery — a direct, encrypted connection between two browsers or devices — is what lets a serverless app like Driftboard sync data straight between peers. Here's what's actually happening underneath.
Two separate planes: signaling and data
It helps to split WebRTC into two jobs that are easy to conflate. The signaling plane is how two peers first find each other and agree on connection details — network addresses, media/data capabilities, and security keys. The data plane is the actual peer-to-peer channel that opens once that handshake is done. Only the first one typically touches a server at all.
The handshake: offer, answer, ICE
To connect, one peer creates a session description (an "offer") and the other responds with an "answer" — these get exchanged through a lightweight signaling server, usually over a WebSocket. Alongside that, both sides gather ICE candidates — a list of possible network paths, discovered via STUN servers that simply tell a device what its own public IP and port look like from the outside. WebRTC then tries those candidate paths and picks the one that actually works, which is most often a direct connection once NAT traversal succeeds.
Crucially, the signaling server only ever relays connection metadata — session descriptions and candidate addresses. It never sees the data that eventually flows over the connection itself, and it isn't involved at all once the two peers are talking directly.
The data channel: encrypted by default
Once connected, WebRTC's RTCDataChannel carries arbitrary binary data over
SCTP, wrapped in DTLS — meaning the channel is encrypted end-to-end between the two peers
by the protocol itself, not as an optional add-on. Neither the signaling server nor anything
else sitting between two directly-connected peers can read the payload.
How Driftboard wires this to a CRDT
Driftboard uses y-webrtc, a provider that connects a
Yjs CRDT document to exactly this
kind of connection. In practice:
- Creating a workspace generates a room name and passphrase, derived locally on your device — nothing is registered anywhere.
- Sharing an invite code or QR gives another device that same room name and passphrase, which it uses to find you through the signaling step and derive the encryption key for the data channel.
- Once connected, any change to the Yjs document — a card moved, a checklist ticked — is broadcast as a small update message directly to every other connected peer.
y-indexeddbpersists the document locally regardless of connection state, so nothing depends on the data channel staying open.
$ npm run electron:dev
# two peers, one workspace
✓ workspace created driftboard-8f2a
✓ invite code RQ7-K2M-91X
$ peer joins with code
✓ WebRTC channel open, encrypted
✓ synced 14 cards, 3 lists — 210ms
The honest limits of a mesh
Two peers have to be online at overlapping times for a change to travel over the data channel — there's no server relaying updates while everyone's offline. Each peer still has its own full, usable local copy at all times via IndexedDB, and the next time two peers are online together, their documents sync automatically. It just means real-time sync is opportunistic rather than guaranteed, which is the trade Driftboard makes in exchange for having no backend at all.
It's also worth noting that a fully connected mesh — every peer directly connected to every other peer — gets expensive as the peer count grows, which is why this architecture fits small, trusted groups far better than large teams.
The public signaling servers Driftboard uses by default are optional — anyone can run
their own with npx y-webrtc-signaling and point the app at it, since the
signaling server never touches board data either way.
Try the sync yourself
Two devices, one invite code, no server holding your data in between.