When I started building CollabDocs, I thought collaborative editing just meant syncing text over WebSockets.
The real challenge appeared when two users edited the same content at the same time and the naive approach started overwriting work.
01The Problem
The first implementation broadcast the full document on every change. That worked for one user but failed under concurrent edits, where the last arriving update silently overwrote the other one.
A timestamp-based last-write-wins patch only made the problem worse because fast typists consistently beat slower ones.
02OT vs CRDT: The Actual Tradeoff
Operational Transform preserves user intent by transforming concurrent operations against each other, but the logic becomes extremely complex once rich text, undo, and nested structures enter the picture.
CRDTs solve the same class of problem differently: the data structure itself is designed so concurrent changes always merge into one valid state regardless of arrival order.
For a solo project, CRDT was the practical choice because I could rely on Y.js instead of maintaining custom transformation logic.
03What I Actually Built
CollabDocs uses a local `Y.Doc` on each client. Changes are encoded as deltas, sent over Socket.io, applied on the server, and broadcast to the rest of the room.
socket.on('doc-update', (update) => {
Y.applyUpdate(ydoc, update);
socket.to(roomId).emit('doc-update', update);
});
ydoc.on('update', (update, origin) => {
if (origin !== 'remote') {
socket.emit('doc-update', update);
}
});The `origin` check prevents echo loops so only locally produced updates are sent back to the server.
04The Debounce Strategy
Real-time collaboration should feel instant, but persisting every keystroke is expensive. Y.js emits updates constantly, so persistence is debounced while live sync remains in memory.
let saveTimeout;
ydoc.on('update', (update) => {
socket.to(roomId).emit('doc-update', update);
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
saveToDatabase(roomId, Y.encodeStateAsUpdate(ydoc));
}, 5000);
});This limits database writes while preserving instant collaborative feedback for active users.
05The Test
The core proof is convergence: two offline clients can edit independently, merge later, and still reach the same final state without data loss.
const doc1 = new Y.Doc();
const doc2 = new Y.Doc();
doc1.getText('content').insert(0, 'Hello');
doc2.getText('content').insert(0, 'World ');
Y.applyUpdate(doc1, Y.encodeStateAsUpdate(doc2));
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1));That is the property the naive full-state approach never guaranteed.
06What I'd Tell Someone Starting This
- Don't roll your own conflict resolution when mature CRDT libraries already exist.
- Understand state vectors and offline sync early.
- Test with concurrent clients from day one, not sequential single-tab flows.
- Treat Redis as the sync layer and MongoDB as the persistence layer.