The problem with locks
Classic collaborative editors used operational transform (OT) — a central server sequencing every operation. It works, but it requires a single coordination point. With Y.js CRDTs, every client can apply changes offline and they'll converge when reconnected, with no server arbitration.
How Y.Doc works
Every Y.Doc is a CRDT document. Y.Text, Y.Map, Y.Array are built-in types. When you insert a character, Y.js assigns it a globally unique ID (client ID + clock). Concurrent inserts are resolved deterministically — no conflict, no data loss.
const doc = new Y.Doc()
const text = doc.getText('main')
// Encode the full document state as a binary Uint8Array
const state = Y.encodeStateAsUpdate(doc)
// Apply a remote update
Y.applyUpdate(doc, remoteUpdate, 'remote')UndoManager per user
A key subtlety: you don't want a global undo stack. If Alice undoes, it should only undo Alice's changes, not Bob's. Y.js solves this with UndoManager, scoped to a specific origin. We create one per socket connection.
const undoMgr = new Y.UndoManager(text, {
trackedOrigins: new Set([socket.id])
})
// Called on Cmd+Z — only undoes this user's edits
undoMgr.undo()The real-world architecture
On our server, every Socket.IO room maps to a Y.Doc. Clients send binary updates over 'yjs:update' events. The server fans them out via Redis Pub/Sub to every node in the cluster — achieving horizontal scaling with zero state drift.