title: Distributed Collaborative Editor
emoji: π
colorFrom: blue
colorTo: indigo
sdk: docker
app_port: 7860
pinned: false
Local-First Distributed Collaborative Editor
A distributed, local-first collaborative document system in TypeScript: many independent replicas (browsers, tabs, devices) edit the same document concurrently, propagate updates through a sync relay, persist locally, and converge under partitions and flaky networksβbuilt for real multi-party collaboration, not a toy two-window demo.
Why this project
This is a distributed collaborative editor: many nodes, one shared document, independent failure and reconnect. The replication story is first-class:
- Replicas β every browser/tab/device is a full copy of document state (multi-primary editing).
- Relay β WebSocket hub fan-out to the replica fleet; merged state for late joiners and recovery.
- Offline-first β IndexedDB persists snapshots and queues unsynced deltas until reconnect.
- Eventual consistency β concurrent edits converge via CRDT-backed merge (Yjs), not last-write-wins on raw strings.
Good fit for demos, coursework, or portfolio work in distributed systems, local-first, or full-stack TypeScript.
Features
| Area | What you get |
|---|---|
| Distributed live editing | N replicas (any mix of browsers, devices, tabs) on one doc; relay fan-out keeps the fleet converging in real time. |
| Offline / partition | Keep editing when disconnected; pending updates flush on reconnect. |
| Multi-tab sync | Same machine, multiple tabs stay aligned via BroadcastChannel. |
| Presence | See who is in the document and who is actively editing. |
| Local durability | Document snapshots and outbox stored in IndexedDB. |
| Server persistence | Relay merges Yjs state; summaries persisted under data/ (gitignored). |
| Dev / demo tools | Test panel: simulate offline, force sync, inspect state vectors, clear local data. |
Architecture
flowchart TB
subgraph replicas["Document replicas (unbounded)"]
R1[Client / tab 1 Β· Y.Doc]
R2[Client / tab 2 Β· Y.Doc]
R3[Client N Β· Y.Doc]
end
subgraph perReplica["Each replica"]
IDB[(IndexedDB + outbox)]
BC[BroadcastChannel]
end
subgraph relay["Central sync relay"]
WS[WebSocket :1234]
MEM[(Merged state + persistence)]
end
R1 --- perReplica
R2 --- perReplica
R1 <-->|intra-browser| R2
R1 & R2 & R3 <-->|Yjs update stream| WS
WS --> MEM
High-level flow
- User edits title/content β Yjs applies local updates.
- Online: updates go to the WebSocket relay and to other tabs on the same origin.
- Offline: updates are queued in IndexedDB; UI shows pending count.
- Reconnect: client sends state / requests catch-up; relay broadcasts merged state; outbox clears after successful persistence.
Tech stack
- Frontend: Next.js (App Router), React 19, Tailwind CSS, Radix UI
- Replication: Yjs (CRDT document model)
- Transport: Node
wsWebSocket server (server/websocket-server.ts) - Browser storage: IndexedDB (
lib/indexeddb.ts) - API: Next.js route handlers for document listing (
app/api/documents)
Prerequisites
- Node.js 18+ (20+ recommended)
- npm, pnpm, or yarn
Quick start
Run two services locally (web app + sync relay). Collaborators can attach as many replicas as you want once both are up.
1. Install dependencies
npm install
# or: pnpm install
2. Start the WebSocket relay
npx tsx server/websocket-server.ts
Default: ws://0.0.0.0:1234 (reachable from other devices on your LAN).
3. Start the web app
In another terminal:
npm run dev
Open http://localhost:3000.
4. Open a document to the fleet
- Click New Document (or pick one from the shared list).
- Share the document URL with collaboratorsβother laptops on your LAN, phones on WiβFi, extra browser profiles, or multiple tabs on one machine.
- Everyone edits at once; the relay broadcasts Yjs updates so all replicas stay aligned. Presence shows who is in the room.
Scale-out pattern: one document ID β many replicas β single relay room β eventual consistency via CRDT merge. Add as many clients as you want; the architecture is N-replica, not pairwise.
Environment variables
| Variable | Default | Description |
|---|---|---|
WS_PORT |
1234 |
WebSocket relay port |
NEXT_PUBLIC_WS_URL |
ws://<hostname>:1234 |
Client WebSocket URL (set if relay is not on localhost) |
Example for a remote relay:
NEXT_PUBLIC_WS_URL=ws://192.168.1.10:1234 npm run dev
Demo ideas (presentations / interviews)
- Multi-replica convergence β spin up several clients on one doc (tabs + devices); show live edits, presence, and connected-peer counts on the relay.
- Network partition β take one replica offline (airplane mode or test panel), keep editing, reconnect the fleet, and show outbox drain + state catch-up.
- Replication paths β contrast inter-tab (
BroadcastChannel) vs cross-network (WebSocket relay) as two channels in one distributed editor. - Late joiner β open a fresh replica mid-session; relay hydrates merged document state so the new node catches up without a full reset.
Project structure
βββ app/
β βββ page.tsx # Document list / home
β βββ doc/[id]/page.tsx # Editor route
β βββ api/documents/ # Shared document metadata API
βββ components/
β βββ document-editor-sync.tsx # Main editor + sync orchestration
β βββ presence-avatars.tsx
β βββ test-panel.tsx # Offline simulation & debug
βββ hooks/
β βββ use-websocket-sync.ts # Cross-device sync
β βββ use-tab-sync.ts # Same-browser tab sync
β βββ use-presence.ts
βββ lib/
β βββ indexeddb.ts # Local persistence + outbox
β βββ shared-documents.ts # Server-side JSON persistence
β βββ offline-queue.ts
βββ server/
β βββ websocket-server.ts # Standalone sync relay
βββ data/ # Created at runtime (gitignored)
Scripts
| Command | Purpose |
|---|---|
npm run dev |
Start Next.js in development |
npm run build |
Production build |
npm run start |
Run production Next.js server |
npm run lint |
ESLint |
npx tsx server/websocket-server.ts |
Start sync relay (required for cross-device sync) |
Limitations (honest scope)
- Single relay β not a multi-node cluster; the WebSocket process is a coordination hub, not consensus (Raft/Paxos).
- No auth β anyone with a document URL can join; suitable for demos, not production multi-tenant security.
- Plain textarea β rich text / comments / permissions are out of scope.
- Yjs handles text merge β the app orchestrates transport, persistence, and UX; conflict-free editing semantics come from Yjsβs CRDT layer.
Acknowledgments
- Yjs β CRDT document replication
- Local-first software β design inspiration