all projects

// FULL-STACK

Collaborative Editor with CRDTs

Real-time collaborative editing via a custom RGA Sequence CRDT

TypeScriptReactViteMonaco EditorWebSocketsNode.jsSQLiteCRDTDistributed Systems

Overview

Google Docs makes real-time collaboration look effortless. Multiple cursors, simultaneous edits, no conflicts. Under that simplicity is one of the hardest problems in distributed systems: how do you let multiple users edit the same document concurrently, without a central lock or coordination server?

The traditional answer is Operational Transformation (OT) — what Google Docs actually uses. But OT requires a central server to serialize operations, and correctness proofs are notoriously tricky. The modern alternative is CRDTs (Conflict-free Replicated Data Types) — data structures that are mathematically guaranteed to converge, regardless of the order operations arrive in.

I built this collaborative editor with CRDTs from scratch — no external CRDT libraries, no OT — to deeply understand how it works.

Core ideas

  • RGA Sequence CRDT implemented from first principles — every character has a unique ID ((siteId, counter)), a value, and a tombstone flag for deletes
  • Out-of-order op handling — incoming inserts are buffered if their predecessor hasn't arrived yet; ghost-deletes are cached for ops referencing characters we haven't seen
  • Eventual consistency — every replica converges to the same document state regardless of network delays or partition order
  • Live remote presence — cursors of other users are tracked and rendered with their names in real-time

Architecture

  • Frontend — React + Vite, with Monaco Editor wrapped to bind into the CRDT layer (Monaco's programmatic-edit API is rich enough that we can patch in/out CRDT ops without fighting the editor)
  • Transport — raw ws WebSockets for low-latency bidirectional sync
  • Backend — Node.js + Express; a lightweight relay that broadcasts ops to all clients in the room
  • Storage — SQLite for rooms + documents, so sessions resume after disconnect
  • Shared types — TypeScript monorepo with crdt-core (pure CRDT logic) shared between client and server, so the same convergence guarantees apply everywhere
┌─ Frontend (React) ──────────────────────────────┐
│   Monaco Editor  ◄►  CRDT Client  ◄►  WebSocket │
└──────────────────────────────────────────────────┘
                              │ ws://
┌─ Backend (Node.js) ──────────┼───────────────────┐
│   SQLite store  ◄►  CRDT merge engine  ◄►  WS   │
└──────────────────────────────────────────────────┘

Why CRDTs (not OT)

  • No central authority — every replica can commit locally and broadcast; convergence happens by construction
  • Easier reasoning — RGA's correctness comes from total ordering of IDs, not from running an OT transformation function over every concurrent edit pair
  • Offline-friendly — local edits commit immediately; sync happens whenever the connection returns
  • Worse memory profile than OT — tombstones never get garbage-collected naively. Trade-off worth knowing about.

Why build it

The CRDT literature is famously dense. Implementing RGA from scratch — handling the buffering, the tombstones, the ghost-deletes, the merge engine — is the only way to actually internalize why the algorithm works. Plus, you end up with a useful tool.