This is an excerpt. The full article includes a live YATA double-linked list simulator — type into two peer editors, toggle network partitions to simulate offline-first edits, and watch Y.js automatically resolve merge conflicts in real time. Read the full interactive version →
The Local-First Wave: Why OT Is Not Enough
Building real-time collaborative software used to require a centralized server orchestrator. For decades, the industry standard was Operational Transformation (OT) — the architecture powering Google Docs.
In OT, every local edit is wrapped into an operation and sent to a central server. The server acts as the single source of truth, recalculating operation coordinates to resolve conflicts and broadcasting adjusted commands back.
The OT Bottleneck: The server must be active 100% of the time, maintaining a complex sequence history buffer. The moment a client drops offline, peer-to-peer sync becomes nearly impossible to reconcile, leading to silent document divergence.
CRDTs (Conflict-free Replicated Data Types) rethink this entirely. Instead of relying on a server to decide conflict priority, CRDT structures are mathematically designed to merge operations in any order without coordination, guaranteeing all clients converge to identical state.
The Mathematical Foundation: Join-Semilattices
To achieve absolute convergence across decentralized peers, a data structure must form a join-semilattice. In practice, your merge functions must conform to three algebraic invariants:
| Property | Formula | Meaning |
|---|---|---|
| Commutativity | A ⊕ B = B ⊕ A |
Operations merge in any sequence order |
| Associativity | (A ⊕ B) ⊕ C = A ⊕ (B ⊕ C) |
Grouping doesn't affect the result |
| Idempotency | A ⊕ A = A |
Receiving the same operation twice doesn't mutate state |
These three properties together mean: you can merge any subset of operations, in any order, any number of times, and always reach the same final document. No coordination server required.
The YATA Engine Under the Hood
Y.js implements YATA (Yet Another Transformation Algorithm) — a highly optimized CRDT designed specifically for text editing.
Under the hood, Y.js represents a document not as a raw string array, but as a double-linked list of Item blocks. Each item contains:
-
id: A unique tuple of(clientId, lamportClock)— globally unique across all peers -
content: The character or string chunk -
originLeft: The ID of the item to the immediate left at creation time -
originRight: The ID of the item to the immediate right at creation time -
deleted: Boolean tombstone flag
These origin pointers act as a permanent spatial anchor. Even if other users concurrently insert characters between the original boundaries, YATA can deterministically sort concurrent insertions using client ID priority — ensuring all replicas converge to identical ordering.
Conflict Resolution Example
Suppose two users type simultaneously at position 5:
-
Peer A inserts
"X"withoriginLeft = "SYS:4" -
Peer B inserts
"Y"withoriginLeft = "SYS:4"
Both have the same originLeft. YATA resolves this by comparing client IDs alphanumerically. If "A" < "B", Peer A's item is sorted before Peer B's — identically on both replicas, regardless of which update arrived first.
Connecting Monaco Editor
Binding Y.js to Monaco Editor is done via the y-monaco package:
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { MonacoBinding } from "y-monaco";
import * as monaco from "monaco-editor";
// 1. Initialize Y.js document
const ydoc = new Y.Doc();
// 2. Connect to WebSocket relay server
const provider = new WebsocketProvider(
"wss://your-relay-server.com",
"document-room-id",
ydoc
);
// 3. Create shared Y.Text type
const ytext = ydoc.getText("monaco-content");
// 4. Bind to Monaco Editor instance
const editor = monaco.editor.create(document.getElementById("editor")!, {
value: "",
language: "typescript"
});
// MonacoBinding syncs ytext ↔ Monaco model bidirectionally
// It also handles cursor decorations and multi-user awareness
const binding = new MonacoBinding(
ytext,
editor.getModel()!,
new Set([editor]),
provider.awareness
);
The MonacoBinding synchronizes Y.js document state with Monaco's internal text model bidirectionally — handling cursor decorations, selection highlights, and presence indicators automatically.
Production Relay Infrastructure
A single Y.js WebSocket server can't scale horizontally — each instance maintains its own in-memory document state. To scale, use Redis Pub/Sub as a shared synchronization backbone:
import { createServer } from "http";
import { WebSocketServer } from "ws";
import { setupWSConnection } from "y-websocket/bin/utils";
import { createClient } from "redis";
const redisPublisher = createClient({ url: process.env.REDIS_URL });
const redisSubscriber = redisPublisher.duplicate();
await redisPublisher.connect();
await redisSubscriber.connect();
const wss = new WebSocketServer({ server: createServer() });
wss.on("connection", (ws, req) => {
// y-websocket handles CRDT sync protocol
setupWSConnection(ws, req, {
// Persistence callback — hook into Redis or PostgreSQL here
persistence: {
bindState: async (docName, ydoc) => {
// Load existing document state from Redis
const state = await redisPublisher.get(`ydoc:${docName}`);
if (state) Y.applyUpdate(ydoc, Buffer.from(state, "base64"));
},
writeState: async (docName, ydoc) => {
// Persist document state on every update
const state = Y.encodeStateAsUpdate(ydoc);
await redisPublisher.set(`ydoc:${docName}`, Buffer.from(state).toString("base64"));
}
}
});
});
With Redis Pub/Sub, any number of WebSocket server instances can participate in the same document room. An update arriving at Server A propagates to Redis, which fans it out to Servers B and C, ensuring all connected peers — regardless of which server they're on — stay synchronized.
Key Optimizations for Production
Awareness Protocol: Y.js includes a built-in
awarenesssystem for sharing ephemeral state (cursor positions, user names, colors) without persisting to the CRDT document. Use this for presence, not your main Y.Doc.State Vector Sync: On reconnect, clients exchange state vectors (
Y.encodeStateVector) to efficiently compute only the missing updates — avoiding full document retransmission.Subdocument Architecture: For large applications, split content across multiple Y.Doc instances (one per section/page). Load subdocuments lazily when accessed.
Engineering Takeaways
- OT requires centralized servers; CRDTs enable true local-first. Offline edits automatically merge on reconnect.
- YATA's origin pointers guarantee deterministic ordering without server coordination.
-
y-websocket+y-monacois the fastest path to production collaborative editing. - Redis Pub/Sub enables horizontal scaling of Y.js WebSocket relay servers.
- State vectors make reconnection efficient — only delta updates are exchanged.
The full article includes a live YATA double-linked list simulator. You can type into both peer editors simultaneously, toggle each peer's network state to create a partition, make offline edits, then click "Resolve & Sync Partition" to watch the CRDT merge algorithm reconcile divergent histories in real time — with the underlying Item linked-list visualized beneath.
Written by Ebenezer Akinseinde — Software Developer & AI Automations Engineer.
Top comments (0)