AboutBlogContactResumeStart a Project
Back to Blog
Building a Real-Time Collaborative Code Editor with WebSockets
Technical
November 5, 2025·9 min read·By Rugved Chandekar

Building a Real-Time Collaborative Code Editor with WebSockets

WebSocketsReal-TimePythonFlask-SocketIO

XGBoost + SHAP: Building an Explainable House Price Predictor (R²=0.88)

XGBoostSHAPFlask APIExplainability

Two users. One file. Changes happening simultaneously from different locations, both appearing instantly on both screens without conflicts. Building real-time collaborative editing sounds simple — until you actually sit down to do it. The synchronization problem is deceptively hard.

The Problem: Concurrent Editing

The core challenge of collaborative editing is handling concurrent edits. User A is on line 15, User B is on line 3. User B inserts a line — which is now line 4. User A's cursor is now pointing at the wrong line. Both users add text at the exact same millisecond. Which one "wins"? Which order do changes merge?

This is the problem Google Docs, Figma, and Notion all had to solve. The academic term is Operational Transformation (OT) — a set of algorithms for merging concurrent edits consistently. My implementation was simpler, but the challenge was real.

The Architecture

The system had three layers:

Browser (A)                    Browser (B)
    │                              │
    │  WebSocket connection         │  WebSocket connection
    ▼                              ▼
┌─────────────────────────────────────┐
│           Flask-SocketIO Server     │
│  - Manages document state           │
│  - Broadcasts changes to all        │
│    connected clients in a room      │
│  - Applies changes in order         │
└─────────────────────────────────────┘
    │
    ▼
┌─────────────────┐
│  In-memory doc  │
│  (Redis for     │
│  multi-instance)│
└─────────────────┘

WebSockets vs HTTP

The key architectural decision: WebSockets instead of polling. HTTP polling — asking "any updates?" every second — would work but creates latency and unnecessary load. WebSockets create a persistent bidirectional connection. The server can push updates to all clients instantly.

# Server-side with Flask-SocketIO
from flask import Flask
from flask_socketio import SocketIO, join_room, emit

app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")

# In-memory document store
documents = {}

@socketio.on('join')
def on_join(data):
    room = data['room']
    join_room(room)
    # Send current document state to the new client
    if room in documents:
        emit('document_state', {'content': documents[room]})

@socketio.on('edit')
def on_edit(data):
    room = data['room']
    change = data['change']  # {type, position, content}
    
    # Apply change to server-side document
    documents[room] = apply_change(documents[room], change)
    
    # Broadcast to ALL other clients in the room
    emit('remote_edit', change, room=room, include_self=False)

The Client-Side: CodeMirror Integration

Rather than build an editor from scratch, I used CodeMirror — a mature code editing library that provides syntax highlighting, indentation, and cursor management. The collaboration layer sat on top:

// Client-side JavaScript
const socket = io();
const editor = CodeMirror(document.getElementById('editor'), {
    lineNumbers: true,
    mode: 'python'
});

// Listen for remote changes
socket.on('remote_edit', function(change) {
    // Suppress our own change event before applying
    isRemoteChange = true;
    editor.replaceRange(
        change.content,
        editor.posFromIndex(change.from),
        editor.posFromIndex(change.to)
    );
    isRemoteChange = false;
});

// Send local changes to server
editor.on('change', function(cm, changeObj) {
    if (isRemoteChange) return;  // Don't echo remote changes
    
    socket.emit('edit', {
        room: roomId,
        change: {
            from: editor.indexFromPos(changeObj.from),
            to: editor.indexFromPos(changeObj.to),
            content: changeObj.text.join('\n'),
            type: changeObj.origin
        }
    });
});

The Hard Part: Conflict Resolution

The naive implementation breaks when two users type at the same time. User A inserts at position 10. User B inserts at position 10 at the same millisecond. The server receives A's change, applies it (position 10 is now shifted by A's insertion), then receives B's change at position 10 — which is now the wrong position.

My solution for this project: operational timestamps with a transform function. Each change carries a timestamp and the revision number it was based on. If a change is based on an old revision, we transform its position to account for all changes that happened since that revision.

This is a simplified version of OT. Full OT implementations (like what Google Docs uses) are significantly more complex — handling every possible combination of concurrent operations is a research problem. For a side project, simple timestamp-based conflict resolution was sufficient.

What This Project Taught Me

Building the collaborative editor gave me a deep appreciation for problems that look simple from the outside. "Two people editing the same document" seems trivial. The distributed consistency problem underneath is genuinely hard.

It also taught me about WebSocket lifecycle management — handling disconnections, reconnections, and the "split brain" problem where a client gets out of sync with the server's state. Building robust real-time systems requires thinking about failure modes that HTTP request/response hides from you.

Working on a real-time application or collaborative tool? This is a space I've built in and enjoy thinking about.

Get In Touch
RC
Rugved Chandekar AI Systems Engineer @ Idyllic Services — WebSockets & Real-Time Systems — IEEE Author