Websocket Project
DodaTech
5 min read
title: "WebSocket Project: Real-Time Collaboration Platform" description: "Build a complete real-time collaboration platform with WebSocket including document editing, chat, presence, and notifications." weight: 29 date: 2026-06-28 lastmod: 2026-06-28 tags: ["apis", "websocket"]
This project brings together everything you have learned about WebSocket. You will build a complete real-time collaboration platform with document editing, chat, user presence, and push notifications.
## Project Overview
Build a collaboration hub where multiple users can edit documents simultaneously, chat in real time, see each other's presence, and receive instant notifications.
## What You'll Build
- WebSocket server with rooms for each document
- Real-time collaborative document editing
- Group and direct messaging
- User presence and online status
- Push notifications system
- Horizontal scaling with Redis
## Why This Project
This project simulates a real-world real-time application similar to Google Docs, Slack, or Notion. It exercises all major WebSocket patterns in a realistic setting.
## Flow Chart
```mermaid
flowchart TD
A[Client] --> B[Load Balancer]
B --> C[WS Server 1]
B --> D[WS Server 2]
C --> E[Redis Pub/Sub]
D --> E
C --> F[(Document Store)]
C --> G[(Message History)]
C --> H[(User Sessions)]
Architecture
Server Implementation
const WebSocket = require('ws');
const Redis = require('ioredis');
const { v4: uuidv4 } = require('uuid');
const server = new WebSocket.Server({ port: 8080 });
const redis = new Redis();
// State stores
const documents = new Map(); // docId -> DocumentState
const rooms = new Map(); // roomId -> Set<WebSocket>
const userSessions = new Map(); // ws -> UserSession
// Redis pub/sub for cross-server sync
const pub = new Redis();
const sub = new Redis();
sub.subscribe('document-updates', 'presence', 'notifications');
sub.on('message', (channel, message) => {
const data = JSON.parse(message);
switch (channel) {
case 'document-updates':
broadcastToRoom(data.docId, data.update, null);
break;
case 'presence':
broadcastToRoom(data.roomId, {
type: 'presence',
users: data.users,
}, null);
break;
case 'notifications':
sendToUser(data.userId, {
type: 'notification',
...data.notification,
});
break;
}
});
server.on('connection', (ws, req) => {
const sessionId = uuidv4();
const user = authenticate(req);
userSessions.set(ws, { ...user, sessionId });
ws.on('message', (data) => handleMessage(ws, JSON.parse(data)));
ws.on('close', () => handleDisconnect(ws));
ws.send(JSON.stringify({
type: 'connected',
sessionId,
user: { id: user.id, name: user.name },
}));
});
function handleMessage(ws, message) {
const user = userSessions.get(ws);
switch (message.type) {
case 'doc:join':
handleDocJoin(ws, user, message.docId);
break;
case 'doc:edit':
handleDocEdit(ws, user, message);
break;
case 'doc:leave':
handleDocLeave(ws, user);
break;
case 'chat:send':
handleChatMessage(ws, user, message);
break;
case 'presence:update':
handlePresenceUpdate(ws, user, message.status);
break;
}
}
function handleDocEdit(ws, user, message) {
const op = {
userId: user.id,
userName: user.name,
type: message.op,
position: message.position,
text: message.text,
timestamp: Date.now(),
};
// Apply to local state
applyOperation(message.docId, op);
// Publish to other servers via Redis
pub.publish('document-updates', JSON.stringify({
docId: message.docId,
update: { type: 'doc:update', ...op },
}));
// Broadcast to local room
broadcastToRoom(message.docId, {
type: 'doc:update',
...op,
}, ws);
}
function broadcastToRoom(roomId, message, exclude) {
const room = rooms.get(roomId);
if (!room) return;
const data = JSON.stringify(message);
room.forEach((client) => {
if (client !== exclude && client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
Client Implementation
class CollaborationClient {
constructor(serverUrl) {
this.serverUrl = serverUrl;
this.activeDoc = null;
this.connect();
}
connect() {
this.ws = new WebSocket(this.serverUrl);
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
this.ws.onclose = () => {
setTimeout(() => this.connect(), 2000);
};
}
handleMessage(message) {
switch (message.type) {
case 'connected':
this.sessionId = message.sessionId;
this.onConnected?.(message.user);
break;
case 'doc:join':
this.activeDoc = message.docId;
this.onDocLoaded?.(message.content, message.users);
break;
case 'doc:update':
this.onDocUpdated?.(message);
break;
case 'chat:message':
this.onChatMessage?.(message);
break;
case 'presence':
this.onPresenceUpdate?.(message.users);
break;
case 'notification':
this.onNotification?.(message);
break;
}
}
joinDocument(docId) {
this.send({ type: 'doc:join', docId });
}
editDocument(op, position, text) {
this.send({
type: 'doc:edit',
docId: this.activeDoc,
op,
position,
text,
});
}
sendChatMessage(text, room) {
this.send({
type: 'chat:send',
text,
room: room || this.activeDoc,
});
}
send(data) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
}
// Application-specific rendering
const client = new CollaborationClient('wss://collab.example.com/ws');
client.onDocLoaded = (content, users) => {
editor.setContent(content);
presencePanel.updateUsers(users);
};
client.onDocUpdated = (update) => {
editor.applyOperation(update);
};
client.onChatMessage = (message) => {
chatPanel.addMessage(message);
};
client.onPresenceUpdate = (users) => {
presencePanel.updateUsers(users);
};
Docker Compose Deployment
version: '3.8'
services:
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- ws-server-1
- ws-server-2
redis:
image: redis:7-alpine
ports:
- "6379:6379"
ws-server-1:
build: ./server
environment:
- REDIS_URL=redis://redis:6379
- SERVER_ID=ws-1
depends_on:
- redis
ws-server-2:
build: ./server
environment:
- REDIS_URL=redis://redis:6379
- SERVER_ID=ws-2
depends_on:
- redis
web-client:
build: ./client
ports:
- "3000:80"
Common Mistakes
| Mistake | Explanation |
|---|---|
| Not using Operational Transformation (OT) | Concurrent document edits without OT cause data loss and conflicts |
| Ignoring cursor synchronization | Users expect to see each others' cursors in collaborative editing |
| Not handling offline edits | Queue edits made while offline and replay them on reconnection |
| Broadcasting edits without merging | Use OT or CRDT algorithms to merge concurrent changes correctly |
| Missing presence cleanup | Remove disconnected users from presence lists promptly |
FAQ
Mini Project
Complete the full collaboration platform. Extend it with document version history, comment threads, rich text formatting (bold, italic, headers), and permission management (view, edit, comment, admin). Deploy with Docker and include a CI/CD pipeline.
What's Next
Learn about Server-Sent Events (SSE)
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro