Skip to content

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

What algorithm should I use for collaborative editing?

Use Operational Transformation (OT) for centralized architectures or CRDT (Conflict-free Replicated Data Types) for peer-to-peer architectures.

How do I handle undo/redo in collaborative editing?

Track operations with version vectors. Undo applies a compensating operation; redo re-applies the original operation.

How do I synchronize cursors between users?

Send cursor position updates as high-frequency WebSocket messages (throttled to 30-60fps). Display remote cursors with user names.

How do I handle document permissions?

Check permissions on the server before allowing document join/edit operations. Store ACLs in the database.

What is the best way to persist document history?

Store every operation in an append-only log. For document snapshots, periodically save compressed state with version markers.

How do I scale collaborative editing to hundreds of users per document?

Use WebSocket rooms per document, batch updates, and consider switching to a CRDT-based approach for very large documents.

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