Building a Real-Time Collaborative Code Editor with CRDTs and WebRTC
Quick Summary (TL;DR)
- CRDTs (Conflict-Free Replicated Data Types) let multiple users edit the same document without a central server, resolving conflicts automatically and deterministically.
- WebRTC provides low-latency peer-to-peer data channels, perfect for propagating CRDT updates directly between browsers.
- Yjs + WebRTC (
y-webrtc) is the fastest path to a working prototype; Automerge is a strong alternative for JSON-like documents.- You still need a signaling server (a lightweight WebSocket) to bootstrap connections — but after that, peers talk directly.
- A mesh topology works well for small teams (≤10 peers); beyond that, consider an SFU or relay server.
Introduction
Real-time collaboration has moved from a novelty to a baseline expectation. Tools like Google Docs and VS Code Live Share have trained users to expect instant, conflict-free editing across the network. But building that experience from scratch is genuinely tricky — you need to solve the distributed systems problem of merging concurrent edits, and the networking problem of getting those edits where they need to go, fast.
This guide walks you through both halves of that problem. You’ll learn how CRDTs eliminate merge conflicts at the data layer, how WebRTC delivers peer-to-peer transport at the network layer, and how to wire them together into a working collaborative code editor using Yjs and CodeMirror.
By the end, you’ll have a clear mental model, working code, and a checklist of production considerations.
Understanding CRDTs for Collaborative Editing
What Are CRDTs?
A CRDT is a data structure that can be replicated across multiple machines, where each replica can be modified independently and concurrently — with a guarantee that all replicas converge to the same state once updates are exchanged. The merge operation is commutative, associative, and idempotent, meaning the order in which updates arrive doesn’t matter.
This is the key insight: instead of preventing conflicts (as locking does) or resolving them after the fact (as Operational Transformation does), CRDTs make conflicts impossible by construction.
Why CRDTs for Code Editors?
For a collaborative code editor, CRDTs offer three decisive advantages:
- No central authority required. Peers can sync directly, which pairs perfectly with WebRTC’s peer-to-peer model.
- Offline-first by default. A peer can keep editing while disconnected, then merge seamlessly on reconnect.
- Deterministic convergence. Every peer ends up with the identical document, regardless of network timing.
Popular CRDT Libraries
| Library | Best For | Editor Bindings | Notes |
|---|---|---|---|
| Yjs | Real-time text, high performance | CodeMirror, Monaco, ProseMirror | Fastest, smallest updates, huge ecosystem |
| Automerge | JSON-like documents, history | Limited text bindings | Rich history/versioning, heavier payloads |
| ShareDB | OT (not CRDT), server-centric | CodeMirror, Quill | Requires central server; mature but not P2P |
For a code editor, Yjs is the pragmatic choice thanks to its Y.Text type and first-class CodeMirror bindings.
WebRTC for Peer-to-Peer Communication
WebRTC Basics
WebRTC is a browser API for real-time communication. It bundles three things: media streams (audio/video), RTCDataChannel for arbitrary data, and the connection machinery (ICE, STUN, DTLS) to make it all work across NATs and firewalls.
For our purposes, only RTCDataChannel matters — it’s a bidirectional, ordered-by-default, encrypted channel between two peers.
Signaling and Connection Establishment
WebRTC can’t bootstrap itself. Before two peers can talk, they must exchange:
- SDP offers/answers — describing codecs and capabilities.
- ICE candidates — network paths each peer can be reached at.
This exchange happens over a signaling channel — typically a WebSocket server. Once signaling completes, the peers connect directly; the server is no longer in the data path.
sequenceDiagram
participant A as Peer A
participant S as Signaling Server
participant B as Peer B
A->>S: Join room "doc-42"
B->>S: Join room "doc-42"
S-->>A: Peer B joined (peerId)
A->>A: Create RTCPeerConnection
A->>A: Create DataChannel + Offer
A->>S: Send SDP offer
S->>B: Forward SDP offer
B->>B: Set remote description
B->>S: Send SDP answer
S->>A: Forward SDP answer
A->>S: Send ICE candidates
S->>B: Forward ICE candidates
B->>S: Send ICE candidates
S->>A: Forward ICE candidates
Note over A,B: Direct P2P DataChannel open
Data Channels for Real-Time Sync
RTCDataChannel is where CRDT updates flow. Yjs’s y-webrtc provider uses it to broadcast binary updates. Because DataChannels ride on SCTP over DTLS, they’re encrypted by default and can be configured for reliable or unreliable delivery — for CRDTs, reliable ordered delivery is the safe default.
Architecture Overview
Components
flowchart TD
subgraph Browser A
CM1[CodeMirror Editor] --> YT1[Yjs Y.Text]
YT1 --> P1[y-webrtc Provider]
end
subgraph Browser B
CM2[CodeMirror Editor] --> YT2[Yjs Y.Text]
YT2 --> P2[y-webrtc Provider]
end
P1 <-->|RTCDataChannel| P2
P1 -.->|Signaling| SIG[WebSocket Signaling Server]
P2 -.->|Signaling| SIG
YT1 -.->|Optional Persistence| IDB[(IndexedDB)]
Data Flow
- A user types in CodeMirror.
- CodeMirror’s Yjs binding translates the change into a
Y.Textoperation. - Yjs emits a binary update.
y-webrtcbroadcasts the update over every open DataChannel.- Remote peers apply the update to their local
Y.Text, which triggers their CodeMirror binding to render the change.
Because Yjs updates are idempotent and commutative, out-of-order arrivals still converge.
Step-by-Step Implementation
Prerequisites
- Node.js 18+ and npm
- Modern browser (Chrome, Firefox, Safari)
- Familiarity with JavaScript/TypeScript and bundlers (Vite recommended)
Step 1: Set Up the Project
npm create vite@latest collab-editor -- --template vanilla-ts
cd collab-editor
npm install yjs y-webrtc codemirror @codemirror/lang-javascript
npm install -D vite
Step 2: Integrate a Code Editor
// src/editor.ts
import { EditorView, basicSetup } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";
export function createEditor(parent: HTMLElement) {
return new EditorView({
doc: "// Start coding together...\n",
extensions: [basicSetup, javascript()],
parent,
});
}
Step 3: Add CRDT for Text Synchronization
Yjs wraps text in a Y.Text type. We bind it to CodeMirror using y-codemirror.next:
npm install y-codemirror.next
import * as Y from "yjs";
import { yCollab } from "y-codemirror.next";
const ydoc = new Y.Doc();
const ytext = ydoc.getText("codemirror");
const view = new EditorView({
extensions: [basicSetup, javascript(), yCollab(ytext, null)],
parent: document.getElementById("editor")!,
});
At this point, edits to ytext are CRDT operations — but they’re still local. Next we wire up transport.
Step 4: Establish WebRTC Connections
We’ll use y-webrtc, which wraps the entire WebRTC lifecycle — signaling, peer discovery, DataChannel management — behind one provider class.
Step 5: Implement Signaling
y-webrtc ships with a public signaling server for development. For production, run your own:
npx y-webrtc-signaling --port 4444
Then point the provider at it:
import { WebrtcProvider } from "y-webrtc";
const provider = new WebrtcProvider("my-room-id", ydoc, {
signaling: ["ws://localhost:4444"],
});
The room ID scopes the session — only peers using the same ID will connect.
Step 6: Sync Editor with CRDT and WebRTC
Here’s the full wiring:
import * as Y from "yjs";
import { WebrtcProvider } from "y-webrtc";
import { EditorView, basicSetup } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { yCollab } from "y-codemirror.next";
const ydoc = new Y.Doc();
const ytext = ydoc.getText("codemirror");
const provider = new WebrtcProvider("my-room-id", ydoc, {
signaling: ["ws://localhost:4444"],
});
new EditorView({
extensions: [
basicSetup,
javascript(),
yCollab(ytext, provider.awareness),
],
parent: document.getElementById("editor")!,
});
The awareness object (part of Yjs’s protocol) tracks cursor positions and user identities — that’s how you get colored remote cursors.
Open the page in two browser tabs and type. You should see edits propagate instantly.
Step 7: Handle Multiple Peers
y-webrtc automatically forms a mesh: every peer connects to every other peer. For a small team this is ideal — no server in the data path, minimal latency. For larger rooms, the number of connections grows as n(n-1)/2, so a 20-person session means 190 connections. Beyond ~10 peers, use a relay server or an SFU to reduce fan-out.
Step 8: Test and Debug
- Two tabs, one machine: the fastest sanity check.
- Two devices, one Wi-Fi: validates ICE/STUN behavior.
- Network throttling: Chrome DevTools → Network → “Slow 3G” to verify convergence under latency.
ydoc.on('update', ...): log updates to see exactly what’s flowing.provider.on('peers', ...): inspect the current peer set.
Comparison of CRDT Libraries
| Feature | Yjs | Automerge | ShareDB (OT) |
|---|---|---|---|
| Topology | P2P or client-server | P2P or client-server | Server-centric |
| Update size | Very small (binary) | Larger (JSON) | Small (ops) |
| CodeMirror bindings | ✅ First-class | ⚠️ Limited | ✅ Mature |
| Offline support | ✅ Native | ✅ Native | ⚠️ Partial |
| History/versioning | Basic | ✅ Rich | Basic |
| Learning curve | Low | Moderate | Moderate |
| Best for | Real-time text | Document apps | Server-authoritative apps |
Verdict: for a collaborative code editor with P2P transport, Yjs is the clear winner.
Best Practices and Considerations
Performance Optimization
- Batch updates. Wrap bursts of edits in
ydoc.transact(() => { ... })to emit a single update. - Use binary encoding. Yjs updates are already compact; avoid JSON serialization.
- Debounce awareness broadcasts. Cursor updates don’t need millisecond fidelity.
- Cap document size. CRDTs scale well, but multi-megabyte files will strain the mesh.
Security
- Authenticate signaling. Require a JWT or session token before joining a room.
- Validate room IDs. Never let a client join an arbitrary room without authorization.
- Encrypt at rest. WebRTC encrypts in transit (DTLS); persist encrypted if storing locally.
- Sanitize awareness data. Treat peer-supplied names/colors as untrusted input.
Scalability
| Peers | Recommended Topology |
|---|---|
| 2–5 | Full mesh (WebRTC P2P) |
| 6–10 | Mesh + relay fallback |
| 11–50 | SFU or relay server |
| 50+ | Hierarchical / server-authoritative |
Offline Support
Add y-indexeddb to persist the document locally:
import { IndexeddbPersistence } from "y-indexeddb";
new IndexeddbPersistence("my-room-id", ydoc);
Peers can now edit offline, and their changes merge automatically when they reconnect.
Conclusion
Building a real-time collaborative code editor is a two-layer problem, and CRDTs plus WebRTC solve each layer cleanly. Yjs handles the hard math of conflict-free merging; WebRTC handles the hard networking of direct peer communication. Together they let you ship a serverless, offline-capable, low-latency editor in a few hundred lines of code.
Start with the mesh topology and y-webrtc, add y-indexeddb for persistence, and graduate to a relay or SFU when your rooms grow past ten peers. The architecture scales with you.
FAQ
What are CRDTs and why are they useful for collaborative editing? CRDTs are data structures that can be replicated across machines, updated independently and concurrently, and merged deterministically. They’re useful for collaboration because they resolve conflicts automatically without a central server, enabling real-time editing even in peer-to-peer or offline scenarios.
How does WebRTC enable peer-to-peer communication?
WebRTC lets browsers communicate directly. Its RTCDataChannel API carries arbitrary data between peers, bypassing a central server once the connection is established. For CRDT sync, this means updates flow directly from editor to editor.
Do I need a signaling server for WebRTC? Yes. Peers must exchange SDP offers/answers and ICE candidates before a direct connection can form. This signaling step requires a server (typically a WebSocket). After signaling, the server is out of the data path entirely.
Can I use CRDTs with WebRTC for a collaborative code editor? Absolutely. CRDTs handle data synchronization and conflict resolution; WebRTC provides the transport. Together they enable a serverless editor where each peer maintains a replica and changes propagate directly.
How do I handle multiple peers in a WebRTC mesh network?
Each peer connects directly to every other peer. This works well up to about 10 peers. Beyond that, the n(n-1)/2 connection count becomes inefficient, so switch to a relay server or SFU.