Rugved Chandekar

Engineering ideas into existence

AboutBlogContactStart a Project
Back to Blog
Collaborative Code Editor
Technical
December 5, 2025·9 min read·By Rugved Chandekar

Building a Real-Time Collaborative Code Editor with WebSockets

WebSocketsSocket.IOFlaskReal-Time

Multiple users, one editor, instant synchronization across every keystroke. Here's the complete architecture of my WebSocket-powered collaborative code editor — built from scratch without any third-party collab SDK.

The Core Challenge: Real-Time State Sync

The hardest part of building a collaborative editor isn't the code editor UI — it's keeping the state consistent across multiple disconnected clients in near real-time. You have two users typing simultaneously. How do you prevent conflicts? How do you handle network lag? How do you recover when one user disconnects?

I solved this with a simple but effective pattern: server as the single source of truth. Every change is pushed to the server via Socket.IO, the server updates the master state, and broadcasts the new state to all other clients in the room.

The Architecture

Client A (types) → Socket.IO event → Flask-SocketIO server
                                    ↓
                              Update room state
                                    ↓
                    Broadcast to all clients in room (except sender)
                                    ↓
                    Client B, C receive → update their editors

Room Management

Every collaboration session is a "room." A room has an ID (UUID), a current code state, and a list of connected users. When a user joins a room:

@socketio.on('join_room')
def on_join(data):
    room_id = data['room_id']
    join_room(room_id)
    # Send current code state to the newly joined user
    if room_id in rooms:
        emit('sync_code', {'code': rooms[room_id]['code']})
    else:
        rooms[room_id] = {'code': '', 'users': []}
    rooms[room_id]['users'].append(request.sid)

Handling Simultaneous Edits

The main edge case: two users type at the same time. My approach was deliberate simplicity — "last write wins" strategy with debouncing on the client side. Every 100ms of inactivity triggers a sync event. This means:

  • You don't get a sync event per keystroke (too much noise)
  • You get synced state within 100ms of stopping typing (fast enough to feel real-time)
  • Conflicts are resolved by whichever update arrives at the server last

For a production system, I'd use Operational Transforms (OT) or CRDTs — but for a 4-person coding session, "last write wins + debounce" works without the added complexity.

The Frontend

I used CodeMirror 6 for the editor component — it supports syntax highlighting across 30+ languages and integrates cleanly with JavaScript events. The editor listens for changes, debounces them, and emits to the server:

let debounceTimer;
editor.on('change', () => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    socket.emit('code_change', {
      room_id: currentRoom,
      code: editor.getValue()
    });
  }, 100);
});

What I Learned About Real-Time Systems

Building this project fundamentally changed how I think about distributed state. A few key lessons:

  1. Network is not reliable. Handle disconnects. Reconnects. Stale state. Always.
  2. The server is the authority. Never trust two clients to agree on state independently.
  3. Simple conflict resolution beats complex concurrency right up to a point. Know where that point is for your use case.

Interested in building real-time applications or custom collaboration tools? Let's talk.

Get In Touch
RC
Rugved Chandekar Full-Stack Developer • WebSocket & Real-Time Systems • Associate Developer Intern