Back to blog
May 10, 2025

Real-Time Collaboration: The Editing Is the Easy Part

CRDTs solve concurrent editing — a library import solves it for you. What nobody warns you about is everything around it: permissions enforced at the socket, presence, offline recovery, and network partitions. Field notes from building Cynoia's collaborative editor.

crdtwebsocketsreal-time

Real-Time Collaboration: The Editing Is the Easy Part

Here is the surprise waiting inside every "build collaborative editing" ticket: the part that sounds hard — merging concurrent edits from 50+ people typing in the same document — is a solved problem you import as a library. The parts that sound like plumbing are where the engineering actually goes: who is allowed to edit, what everyone else sees while you type, and what happens when the network lies to you.

At Cynoia we built a collaborative editor supporting 50+ concurrent users on Yjs (a CRDT implementation), Socket.IO, Redis, and NestJS. I've written a full deep dive on the merge layer — why naive socket relaying diverges, OT versus CRDTs, and the local-first architecture. This post is about the rest of the iceberg.

The Merge Layer, in Two Sentences

CRDTs give every character a stable identity so concurrent edits merge to the same result on every replica, in any order, with no central arbiter — that is why we chose them over Operational Transformation, which welds you to a server that re-orders and rewrites every operation. If that trade-off interests you, the deep dive earns it properly; from here on, assume the document converges and look at what still breaks.

Permissions: The Socket Is a Second Front Door

A CRDT happily merges edits from anyone — authorization is explicitly not its job, which means it is entirely yours. We built granular document-level RBAC:

  • Owner — full control (edit, share, delete)
  • Editor — can edit content
  • Viewer — read-only access
  • Commenter — can comment but not edit

The detail that matters: permissions are enforced on both the WebSocket connection and the HTTP API. Checking only the REST layer is a classic hole — a "viewer" who can open a socket to the document room can otherwise push updates straight past your carefully guarded save endpoint. In a merge-anywhere architecture, the transport is a second front door, and it needs the same lock.

Persistence Without Write Storms

Persisting every keystroke of 50 concurrent editors is a self-inflicted denial of service on your own database. Two decisions kept write pressure sane:

  1. Batch and flush — updates accumulate in memory and flush to Postgres every few seconds or on document close, not per keystroke
  2. Tier the reads — active documents live in server memory, recently-open ones in Redis (1-hour TTL), Postgres as the source of truth

Together they cut database load by roughly 40% versus the naive write-through path.

The Hard Parts Checklist

What actually consumed the engineering time, in the order it surprised us:

  • Presence — cursors, selections, and who-is-here are ephemeral state: they must not be merged or persisted like document content, so they need their own protocol alongside the CRDT updates
  • Offline and reconnect — edits made during a disconnect have to survive locally and merge cleanly on return, and the UI has to be honest about sync state instead of pretending
  • Permission changes mid-session — revoking an editor who has a live socket and a local replica is nothing like revoking an API token
  • Network partitions — two server nodes can each be serving editors of the same document; the design has to make that safe rather than rare

Every one of these exists because the merge layer works. Solve editing and you have not built a collaborative editor — you have qualified for the real problems.

Key Takeaway

Real-time collaboration is deceptively complex, but the complexity is not where the demos point. The editing itself (CRDTs) is the easy part. The hard parts are presence awareness, offline support, permission management, and handling network partitions gracefully — budget for the iceberg, not the tip.