CRDTs guarantee strong eventual consistency by mathematically structuring state mutations so that any two clients that receive the same set of updates—in any arbitrary order—will always converge to the exact same document state without central locking or server-side coordinate translation.
CRDTs vs Operational Transformation (OT)
Early collaborative systems (like Google Docs) pioneered Operational Transformation. However, OT requires all edits to pass sequentially through a single authoritative central server to transform character index offsets. If a user goes offline or packets arrive out of order, OT complexity explodes exponentially.
| Feature | Operational Transformation (OT) | CRDTs (Yjs / Automerge) |
|---|---|---|
| Central Server Requirement | Mandatory authoritative sequencer | Optional (works peer-to-peer or client-server) |
| Offline Capability | Extremely hard; large rebase conflicts | Native; merge weeks of offline work seamlessly |
| Complexity | Combinatorial transformation functions ($O(N^2)$) | Mathematical convergence axioms ($O(1)$) |
| Network Transports | Strict FIFO delivery required | Idempotent; works over WebSockets, WebRTC, BLE |
Building the Horizontally Scalable Yjs WebSocket Server
When scaling across multiple Node.js Kubernetes pods, clients in the same document room might connect to different server instances. We bridge instances using Redis Pub/Sub channels:
// server.ts - Scalable Yjs WebSocket Server with Redis PubSub
import { WebSocketServer, WebSocket } from "ws";
import * as Y from "yjs";
import * as syncProtocol from "y-protocols/sync";
import * as awarenessProtocol from "y-protocols/awareness";
import Redis from "ioredis";
const pub = new Redis(process.env.REDIS_URL);
const sub = new Redis(process.env.REDIS_URL);
const docs = new Map<string, Y.Doc>();
function getOrCreateDoc(docName: string): Y.Doc {
let doc = docs.get(docName);
if (!doc) {
doc = new Y.Doc();
docs.set(docName, doc);
// Listen to local document updates and publish to Redis cluster
doc.on("update", (update: Uint8Array, origin: any) => {
if (origin !== "redis") {
const updateBase64 = Buffer.from(update).toString("base64");
pub.publish(`yjs:${docName}`, updateBase64);
}
});
// Subscribe to multi-instance updates
sub.subscribe(`yjs:${docName}`);
}
return doc;
}
sub.on("message", (channel, message) => {
const docName = channel.replace("yjs:", "");
const doc = docs.get(docName);
if (doc) {
const update = Buffer.from(message, "base64");
Y.applyUpdate(doc, update, "redis");
}
});
export function setupWebSocketCollaboration(wss: WebSocketServer) {
wss.on("connection", (conn: WebSocket, req) => {
const docName = req.url?.slice(1) || "default-room";
const doc = getOrCreateDoc(docName);
// Step 1: Send initial SyncStep1 message to client
const encoder = syncProtocol.createSyncStep1Message(doc);
conn.send(encoder);
// Step 2: Handle incoming binary sync messages
conn.on("message", (message: ArrayBuffer) => {
const uint8 = new Uint8Array(message);
syncProtocol.readSyncMessage(uint8, doc, conn);
});
conn.on("close", () => {
// Clean up memory if room is empty
if (doc.isDestroyed) docs.delete(docName);
});
});
}Whenever a client sends a delta update to Server A, it is applied to the local memory document and instantly fanned out via Redis to Server B and C, ensuring zero-latency multi-pod synchronization.
React 19 Client Integration & Live Cursor Awareness
Presence features (such as live multiplayer cursor coordinates, selection highlights, and user avatars) are ephemeral. The y-protocols/awareness module handles broadcast without dirtying persistent document state:
// useCollaborativeEditor.ts - React 19 Client Integration
import { useEffect, useState } from "react";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
export function useCollaborativeEditor(roomId: string, user: { name: string; color: string }) {
const [ydoc] = useState(() => new Y.Doc());
const [provider, setProvider] = useState<WebsocketProvider | null>(null);
const [awarenessUsers, setAwarenessUsers] = useState<any[]>([]);
useEffect(() => {
const wsProvider = new WebsocketProvider(
process.env.NEXT_PUBLIC_WS_ENDPOINT || "wss://ws.endurancesoftwares.com",
roomId,
ydoc
);
// Broadcast user presence/cursor
wsProvider.awareness.setLocalStateField("user", {
name: user.name,
color: user.color,
});
wsProvider.awareness.on("change", () => {
const states = Array.from(wsProvider.awareness.getStates().values());
setAwarenessUsers(states.map((s: any) => s.user).filter(Boolean));
});
setProvider(wsProvider);
return () => {
wsProvider.destroy();
ydoc.destroy();
};
}, [roomId, ydoc, user.name, user.color]);
return { ydoc, provider, awarenessUsers };
}Document Persistence: Compact Snapshots in PostgreSQL & S3
CRDT updates are incremental binary blobs. Storing every keystroke in a relational database creates unbounded row bloat. Follow the Compaction Snapshot Pattern:
- Write Buffer: Buffer raw Yjs update fragments in Redis for up to 10 seconds.
- Squash & Encode: Periodically run
Y.encodeStateAsUpdate(ydoc)to compact thousands of tiny edits into a single compressed binary state buffer. - PostgreSQL BLOB Storage: Store the merged state in a
byteacolumn or upload to AWS S3 for long-term document history versioning.
Offline Resilience & Instant Reconnection
Using IndexedDB persistence adapters (like y-indexeddb), users can edit documents on airplanes or in unstable mobile networks. When connectivity resumes, the WebSocket provider automatically sends a compact vector clock diff, reconciling days of offline edits within milliseconds without prompt dialogs or loss of work.
Real-Time Architecture Checklist
✓ Binary WebSockets protocol used instead of JSON strings
✓ Redis Pub/Sub enables seamless multi-server cluster routing
✓ Ephemeral awareness layer separates cursors from document state
✓ IndexedDB client caching enables 100% offline document editing
✓ Periodic state compaction prevents CRDT memory bloat
✓ TLS / WSS encryption with JWT authorization tokens on handshake
✓ Graceful disconnection handling and document memory cleanup
✓ Vector clock conflict tests automated in test suite
Build Collaborative Web Apps with Endurance Softwares
Our senior full-stack engineers build real-time multi-user canvases, diagramming tools, and interactive SaaS portals powered by WebSockets and CRDTs.
Discuss Your Real-Time ProjectFrequently Asked Questions
How does Yjs prevent memory leaks on long-running documents?
Yjs uses a highly optimized struct store. Calling Y.encodeStateAsUpdate() purges deleted metadata and compacts item tombstones, keeping active memory under a few megabytes even for book-length documents.
Can CRDTs handle rich text with formatting attributes?
Yes! Y.Text supports rich-text formatting (bold, italics, links, custom attributes) natively and integrates seamlessly with modern rich text editors like ProseMirror, TipTap, Quill, and Lexical.
What happens if two users edit the exact same word simultaneously?
CRDTs deterministically interleave the characters based on their unique client IDs and clock sequence numbers. No characters are ever lost, and both screens arrive at identical strings.