| --- |
| 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 |
|
|
| ```mermaid |
| 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** |
|
|
| 1. User edits title/content β Yjs applies local updates. |
| 2. **Online:** updates go to the WebSocket relay and to other tabs on the same origin. |
| 3. **Offline:** updates are queued in IndexedDB; UI shows pending count. |
| 4. **Reconnect:** client sends state / requests catch-up; relay broadcasts merged state; outbox clears after successful persistence. |
|
|
| --- |
|
|
| ## Tech stack |
|
|
| - **Frontend:** [Next.js](https://nextjs.org/) (App Router), React 19, Tailwind CSS, Radix UI |
| - **Replication:** [Yjs](https://github.com/yjs/yjs) (CRDT document model) |
| - **Transport:** Node `ws` WebSocket 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 |
|
|
| ```bash |
| npm install |
| # or: pnpm install |
| ``` |
|
|
| ### 2. Start the WebSocket relay |
|
|
| ```bash |
| 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: |
|
|
| ```bash |
| npm run dev |
| ``` |
|
|
| Open [http://localhost:3000](http://localhost:3000). |
|
|
| ### 4. Open a document to the fleet |
|
|
| 1. Click **New Document** (or pick one from the shared list). |
| 2. **Share the document URL** with collaboratorsβother laptops on your LAN, phones on WiβFi, extra browser profiles, or multiple tabs on one machine. |
| 3. 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: |
|
|
| ```bash |
| NEXT_PUBLIC_WS_URL=ws://192.168.1.10:1234 npm run dev |
| ``` |
|
|
| --- |
|
|
| ## Demo ideas (presentations / interviews) |
|
|
| 1. **Multi-replica convergence** β spin up several clients on one doc (tabs + devices); show live edits, presence, and connected-peer counts on the relay. |
| 2. **Network partition** β take one replica offline (airplane mode or test panel), keep editing, reconnect the fleet, and show outbox drain + state catch-up. |
| 3. **Replication paths** β contrast **inter-tab** (`BroadcastChannel`) vs **cross-network** (WebSocket relay) as two channels in one distributed editor. |
| 4. **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](https://github.com/yjs/yjs) β CRDT document replication |
| - [Local-first software](https://www.inkandswitch.com/local-first/) β design inspiration |
|
|