A production-ready, Google Docs-like collaborative rich-text editor built with modern web technologies. Multiple users can edit documents simultaneously with live cursors, selections, and real-time synchronization.
| Feature | Description |
|---|---|
| 🔄 Real-time Collaboration | Multiple users can edit simultaneously with automatic conflict resolution using Yjs CRDT |
| 👆 Live Cursors & Selections | See other users' cursors and text selections in real-time with colored indicators |
| 👥 User Presence | View list of connected users with colored avatars and usernames |
| 💾 Document Persistence | Documents are automatically saved to LevelDB (or MongoDB) |
| 📄 Multiple Documents | Support for multiple documents via URL routing (/doc/:docId) |
| ✏️ Rich Text Editing | Full-featured editor with formatting options (bold, italic, underline, colors, fonts, etc.) |
| 🔌 Connection Status | Visual indicators for connection state and last saved time |
| 🌙 Dark Mode | Beautiful dark mode support for comfortable editing |
- React 18 + Vite + TypeScript
- TipTap - Headless rich text editor framework
- Yjs - CRDT for conflict-free document merging
- y-prosemirror - Yjs integration with ProseMirror (TipTap's core)
- Socket.io Client - WebSocket communication
- Node.js + Express + TypeScript
- Socket.io - WebSocket server
- Yjs - Document synchronization
- LevelDB - Document persistence (MongoDB option available)
realtime-collaborative-editor/
├── backend/
│ ├── src/
│ │ ├── server.ts # Express + Socket.io server
│ │ ├── yjs-provider.ts # Yjs document management & Socket.io bridging
│ │ ├── persistence.ts # LevelDB persistence layer
│ │ ├── routes.ts # REST API routes
│ │ └── types.ts # TypeScript types
│ ├── package.json
│ └── tsconfig.json
│
├── frontend/
│ ├── src/
│ │ ├── main.tsx # React entry point
│ │ ├── App.tsx # Router setup
│ │ ├── EditorPage.tsx # Main editor page
│ │ ├── components/
│ │ │ ├── RichEditor.tsx # TipTap editor with Yjs binding
│ │ │ ├── Toolbar.tsx # Rich text formatting toolbar
│ │ │ └── PresenceBar.tsx # User presence list
│ │ ├── extensions/
│ │ │ ├── YjsExtension.ts # Yjs integration extension
│ │ │ ├── FontSize.ts # Custom font size extension
│ │ │ └── FontFamily.ts # Custom font family extension
│ │ ├── services/
│ │ │ └── socketProvider.ts # Socket.io + Yjs provider
│ │ └── contexts/
│ │ └── ThemeContext.tsx # Dark mode context
│ ├── package.json
│ └── vite.config.ts
│
└── README.md
- Node.js 18+ and npm
-
Install all dependencies:
npm run install:all
Or manually:
npm install cd backend && npm install cd ../frontend && npm install
Option 1: Run both frontend and backend together (recommended for development):
npm run devThis starts:
- Backend server on
http://localhost:3001 - Frontend dev server on
http://localhost:3000
Option 2: Run separately:
Backend:
cd backend
npm run devFrontend:
cd frontend
npm run dev- Start the dev server:
npm run dev - Open
http://localhost:3000in your browser - Enter a username (or use saved one)
- A new document will be created automatically, or navigate to
/doc/:docIdfor a specific document - Open the same URL in multiple browser tabs/windows to test collaboration
- Type in one tab and see changes appear in real-time in other tabs
- Move your cursor and see it appear as a colored indicator in other tabs
# Terminal 1: Start the server
npm run dev
# Browser 1: Open http://localhost:3000/doc/test-doc-123
# Enter username: "Alice"
# Type: "Hello from Alice!"
# Browser 2: Open http://localhost:3000/doc/test-doc-123
# Enter username: "Bob"
# Type: "Hello from Bob!"
# You should see both messages appear in both browsers in real-time
# Cursors and selections should be visible with colored indicators- Client connects → Sends
join-docwith{ docId, username, color } - Server responds → Sends
sync-step1with document state vector - Client responds → Sends
sync-step2with its state vector - Server sends missing updates →
sync-updatewith binary Yjs update - Ongoing updates → Clients send
doc-updatewith binary Yjs updates, server broadcasts to room
- Clients track their cursor position and selection in local awareness state
- Awareness updates are sent to server via
awareness-updateevent - Server broadcasts awareness updates to all clients in the document room
- Frontend renders remote cursors using
yCursorPluginfrom y-prosemirror
- Documents are persisted to LevelDB in
./data/directory - Updates are debounced (1 second) to avoid excessive writes
- On server start, persisted documents are loaded into memory
- Documents are cleaned up from memory after 60 seconds of no clients (but remain persisted)
To use MongoDB instead of LevelDB:
-
Install MongoDB driver:
cd backend npm install mongodb -
Update
backend/src/persistence.ts:import { MongoClient, Db, Collection } from 'mongodb'; export class DocumentPersistence { private client: MongoClient; private db: Db; private documents: Collection; private metadata: Collection; constructor(connectionString: string = 'mongodb://localhost:27017') { this.client = new MongoClient(connectionString); this.db = this.client.db('collaborative-editor'); this.documents = this.db.collection('documents'); this.metadata = this.db.collection('metadata'); } async loadDocument(docId: string): Promise<Uint8Array | null> { const doc = await this.documents.findOne({ _id: docId }); return doc ? Buffer.from(doc.data) : null; } async saveDocument(docId: string, doc: Y.Doc): Promise<void> { const update = Y.encodeStateAsUpdate(doc); await this.documents.updateOne( { _id: docId }, { $set: { data: Buffer.from(update), updatedAt: new Date() } }, { upsert: true } ); } // ... similar for metadata methods }
-
Update
backend/src/server.tsto pass MongoDB connection string
npm run buildThis builds both frontend and backend. The backend will serve the frontend static files in production mode.
| Variable | Description | Default |
|---|---|---|
PORT |
Backend server port | 3001 |
FRONTEND_PORT |
Frontend port | 3000 |
NODE_ENV |
Set to production for production mode |
development |
VITE_SOCKET_URL |
Frontend Socket.io URL | http://localhost:3001 |
- Create a Heroku app
- Set buildpacks: Node.js
- Set environment variables:
NODE_ENV=production PORT=3001 - Deploy:
git push heroku main
- Frontend: Deploy to Vercel (static export)
- Backend: Deploy to Railway, Render, or similar Node.js hosting
- Update
VITE_SOCKET_URLto point to your backend URL
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/doc/:docId |
Get document metadata |
POST |
/api/doc/:docId |
Create/update document metadata |
GET |
/api/health |
Health check |
| Event | Description |
|---|---|
join-doc |
Join a document room |
sync-step2 |
Send state vector for sync |
doc-update |
Send document update |
awareness-update |
Send awareness state |
| Event | Description |
|---|---|
sync-step1 |
Initial state vector |
sync-update |
Missing document updates |
doc-update |
Document update from another client |
awareness-update |
Awareness state from other clients |
user-joined |
User joined notification |
user-left |
User left notification |
| Issue | Solution |
|---|---|
| Connection issues | Check that backend is running on port 3001 and frontend can reach it |
| Cursors not showing | Ensure awareness updates are being sent/received (check browser console) |
| Changes not syncing | Verify Yjs document updates are being applied (check server logs) |
| Persistence not working | Ensure ./data/ directory is writable |
Key implementation details are commented in:
backend/src/yjs-provider.ts- Yjs sync protocol and awareness handlingfrontend/src/services/socketProvider.ts- Client-side Yjs providerfrontend/src/extensions/YjsExtension.ts- Yjs integration with TipTap
For a simpler setup, you could use the official y-websocket provider instead of the custom Socket.io provider. However, this implementation uses a custom provider for better control and Socket.io integration.
To use y-websocket:
npm install y-websocketThen replace the Socket.io provider with y-websocket in the frontend.
MIT
- Yjs - CRDT library for real-time collaboration
- TipTap - Headless rich text editor framework
- Socket.io - Real-time bidirectional event-based communication
Made with ❤️ for collaborative editing