Claude Code for Real-Time Collaboration: CRDTs, Operational Transform, and Shared Editing — Claude Skills 360 Blog
Blog / Development / Claude Code for Real-Time Collaboration: CRDTs, Operational Transform, and Shared Editing
Development

Claude Code for Real-Time Collaboration: CRDTs, Operational Transform, and Shared Editing

Published: August 4, 2026
Read time: 9 min read
By: Claude Skills 360

Real-time collaborative editing — multiple users editing the same document simultaneously — requires conflict resolution that merges concurrent changes correctly. Claude Code generates Yjs CRDT implementations, Y-WebSocket server configurations, and the awareness protocol that shows who is where in the document.

Yjs Shared Document

Add collaborative editing to our markdown editor.
Multiple users should be able to edit simultaneously.
Show each user's cursor position in real-time.

Server: Y-WebSocket Provider

// server/collaboration.ts
import { WebSocketServer } from 'ws';
import { setupWSConnection } from 'y-websocket/bin/utils';
import { createServer } from 'http';

// y-websocket handles all CRDT synchronization automatically
const httpServer = createServer();
const wss = new WebSocketServer({ server: httpServer });

wss.on('connection', (conn, req) => {
  // Extract room from URL: /collab/doc-123
  const docName = req.url?.slice(1) ?? 'default';

  setupWSConnection(conn, req, {
    docName,
    gc: true, // Garbage collect old tombstones
  });
});

httpServer.listen(4444, () => console.log('Y-WebSocket server on :4444'));

For production: use y-redis to persist documents across server restarts and enable horizontal scaling.

# y-redis: uses Redis for storage + pub/sub for multi-server sync
npm install y-redis @y-redis/server

# Server with Redis backend
// server/collaboration-redis.ts
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { createWSServer } from '@y-redis/server';

const httpServer = createServer();
const wss = new WebSocketServer({ server: httpServer });

const { handleConnection } = createWSServer({
  redis: process.env.REDIS_URL!,
  storage: 'redis',
});

wss.on('connection', handleConnection);
httpServer.listen(4444);

Client: Collaborative Editor

// components/CollaborativeEditor.tsx
'use client';

import { useEffect, useRef, useState } from 'react';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { QuillBinding } from 'y-quill';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';

interface Collaborator {
  clientId: number;
  name: string;
  color: string;
  cursor: number;
}

export function CollaborativeEditor({ docId, userName }: { docId: string; userName: string }) {
  const editorRef = useRef<HTMLDivElement>(null);
  const [collaborators, setCollaborators] = useState<Collaborator[]>([]);
  const [connected, setConnected] = useState(false);
  const [synced, setSynced] = useState(false);

  useEffect(() => {
    if (!editorRef.current) return;

    // Yjs document — the shared state
    const ydoc = new Y.Doc();
    const ytext = ydoc.getText('quill');

    // WebSocket connection to y-websocket server
    const provider = new WebsocketProvider(
      process.env.NEXT_PUBLIC_COLLAB_WS_URL!,
      docId,
      ydoc,
    );

    // Awareness: share cursor position and user identity
    const awareness = provider.awareness;
    const userColor = `hsl(${Math.random() * 360}, 70%, 50%)`;

    awareness.setLocalState({
      name: userName,
      color: userColor,
      cursor: null,
    });

    // Track connected collaborators
    awareness.on('change', () => {
      const states = Array.from(awareness.getStates().entries())
        .filter(([clientId]) => clientId !== awareness.clientID)
        .map(([clientId, state]) => ({
          clientId,
          name: state.name ?? 'Anonymous',
          color: state.color ?? '#999',
          cursor: state.cursor ?? 0,
        }));
      setCollaborators(states);
    });

    provider.on('status', ({ status }: { status: string }) => {
      setConnected(status === 'connected');
    });

    provider.on('sync', (isSynced: boolean) => {
      setSynced(isSynced);
    });

    // Initialize Quill editor
    const quill = new Quill(editorRef.current, {
      modules: { toolbar: [['bold', 'italic'], [{ list: 'ordered' }, { list: 'bullet' }], ['link']] },
      theme: 'snow',
    });

    // Bind Quill to the Yjs shared text
    const binding = new QuillBinding(ytext, quill, awareness);

    // Update awareness cursor on selection change
    quill.on('selection-change', (range) => {
      awareness.setLocalStateField('cursor', range?.index ?? null);
    });

    return () => {
      binding.destroy();
      provider.disconnect();
      ydoc.destroy();
    };
  }, [docId, userName]);

  return (
    <div>
      <div className="collab-status">
        <span className={`status-dot ${connected ? 'connected' : 'disconnected'}`} />
        {connected ? (synced ? 'Synced' : 'Syncing...') : 'Connecting...'}

        {collaborators.length > 0 && (
          <div className="collaborators">
            {collaborators.map(c => (
              <div key={c.clientId} className="collaborator-badge" style={{ backgroundColor: c.color }}>
                {c.name[0].toUpperCase()}
              </div>
            ))}
            <span>{collaborators.length} other{collaborators.length > 1 ? 's' : ''} editing</span>
          </div>
        )}
      </div>

      <div ref={editorRef} />
    </div>
  );
}

CodeMirror 6 Collaborative Editing

Add collaborative editing to our code editor built on CodeMirror 6.
Multiple users edit the same file with shared cursor positions.
// components/CollaborativeCodeEditor.tsx
import { useEffect, useRef } from 'react';
import { EditorView, basicSetup } from 'codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { yCollab, yUndoManagerKeymap } from 'y-codemirror.next';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { keymap } from '@codemirror/view';

export function CollaborativeCodeEditor({ fileId, userId, userName }: {
  fileId: string;
  userId: string;
  userName: string;
}) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const ydoc = new Y.Doc();
    const ytext = ydoc.getText('codemirror');

    const provider = new WebsocketProvider(
      process.env.NEXT_PUBLIC_COLLAB_WS_URL!,
      `file:${fileId}`,
      ydoc,
    );

    const colors = ['#e91e63', '#2196f3', '#4caf50', '#ff9800', '#9c27b0'];
    const userColor = colors[parseInt(userId.slice(-1), 16) % colors.length];

    provider.awareness.setLocalStateField('user', {
      name: userName,
      color: userColor,
      colorLight: userColor + '33', // Transparent for selection highlight
    });

    const view = new EditorView({
      doc: ytext.toString(),
      extensions: [
        basicSetup,
        javascript({ typescript: true }),
        yCollab(ytext, provider.awareness),
        keymap.of(yUndoManagerKeymap),
        EditorView.theme({
          '&': { height: '100%' },
          '.cm-scroller': { overflow: 'auto' },
        }),
      ],
      parent: containerRef.current,
    });

    return () => {
      view.destroy();
      provider.disconnect();
      ydoc.destroy();
    };
  }, [fileId, userId, userName]);

  return <div ref={containerRef} style={{ height: '100%', width: '100%' }} />;
}

Shared State Beyond Text

Use Yjs for shared state in a collaborative whiteboard:
shapes that multiple users can add, move, and delete simultaneously.
// For structured data: Y.Map and Y.Array are CRDTs too
const ydoc = new Y.Doc();
const shapes = ydoc.getMap<{
  id: string;
  type: 'rect' | 'circle' | 'text';
  x: number;
  y: number;
  width: number;
  height: number;
  color: string;
}>('shapes');

// Add shape — syncs to all peers automatically
function addShape(shape: Shape) {
  ydoc.transact(() => {
    shapes.set(shape.id, shape);
  });
}

// Move shape — concurrent moves merge correctly (last writer wins for position)
function moveShape(id: string, x: number, y: number) {
  const shape = shapes.get(id);
  if (shape) {
    ydoc.transact(() => {
      shapes.set(id, { ...shape, x, y });
    });
  }
}

// Delete — handled correctly even with concurrent modifications
function deleteShape(id: string) {
  ydoc.transact(() => {
    shapes.delete(id);
  });
}

// Subscribe to changes
shapes.observe((event) => {
  event.changes.keys.forEach((change, key) => {
    if (change.action === 'add' || change.action === 'update') {
      renderShape(shapes.get(key)!);
    } else if (change.action === 'delete') {
      removeShape(key);
    }
  });
});

For the WebSocket infrastructure that scales the collaboration server, see the WebSocket scaling guide. For building document editing with rich formatting, see the React 19 guide for the latest form/action patterns. The Claude Skills 360 bundle includes real-time collaboration skill sets for Yjs, CRDT patterns, and editor integration. Start with the free tier to try collaborative editing scaffolding.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free