skills/real-time-collaboration-engine/SKILL.md
Build real-time collaborative editing with WebSockets, OT/CRDT conflict resolution, and presence awareness. Implements cursor tracking, optimistic updates, and offline sync. Use for collaborative editors, whiteboards, video editing. Activate on "real-time collaboration", "WebSocket sync", "multiplayer editing", "CRDT", "presence awareness". NOT for simple chat, request-response APIs, or single-user apps.
npx skillsauth add curiositech/windags-skills real-time-collaboration-engineInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expert in building Google Docs-style collaborative editing with WebSockets, conflict resolution, and presence awareness.
✅ Use for:
❌ NOT for:
Need real-time collaboration?
├── Text editing? → Operational Transform (OT)
├── JSON data structures? → CRDTs
├── Cursor tracking only? → Simple WebSocket + presence
├── Offline-first? → CRDTs (better offline merge)
└── No conflicts possible? → Basic broadcast
| Strategy | Best For | Complexity | Offline Support | |----------|----------|------------|-----------------| | Operational Transform (OT) | Text, ordered sequences | High | Limited | | CRDTs | JSON objects, sets | Medium | Excellent | | Last-Write-Wins | Simple state | Low | Basic | | Three-Way Merge | Git-style editing | High | Good |
Timeline:
Novice thinking: "Send every change immediately for real-time feel"
Problem: Network floods with tiny messages, poor performance.
Wrong approach:
// ❌ Sends message on every keystroke
function Editor() {
const handleChange = (text: string) => {
socket.emit('text-change', { text }); // Every keystroke!
};
return <textarea onChange={(e) => handleChange(e.target.value)} />;
}
Why wrong: 100 WPM typing = 500 messages/minute = network congestion.
Correct approach:
// ✅ Batches changes every 200ms
function Editor() {
const [pendingChanges, setPendingChanges] = useState<Change[]>([]);
useEffect(() => {
const interval = setInterval(() => {
if (pendingChanges.length > 0) {
socket.emit('text-batch', { changes: pendingChanges });
setPendingChanges([]);
}
}, 200);
return () => clearInterval(interval);
}, [pendingChanges]);
const handleChange = (change: Change) => {
setPendingChanges(prev => [...prev, change]);
};
return <textarea onChange={handleChange} />;
}
Impact: 500 messages/minute → 5 messages/second (90% reduction).
Problem: Concurrent edits cause data loss or corruption.
Symptom: Users see their changes disappear, documents become inconsistent.
Wrong approach:
// ❌ Last write wins, overwrites concurrent changes
socket.on('text-change', ({ userId, text }) => {
setDocument(text); // Loses concurrent edits!
});
Why wrong: If User A and B edit simultaneously, one change is lost.
Correct approach (OT):
// ✅ Operational Transform for text
import { TextOperation } from 'ot.js';
socket.on('operation', ({ userId, operation, revision }) => {
const transformed = transformOperation(
operation,
pendingOperations,
revision
);
applyOperation(transformed);
incrementRevision();
});
function transformOperation(
incoming: Operation,
pending: Operation[],
baseRevision: number
): Operation {
// Transform incoming against pending operations
let transformed = incoming;
for (const op of pending) {
transformed = TextOperation.transform(transformed, op)[0];
}
return transformed;
}
Correct approach (CRDT):
// ✅ CRDT for JSON objects
import * as Y from 'yjs';
const ydoc = new Y.Doc();
const ytext = ydoc.getText('document');
// Automatically handles conflicts
ytext.insert(0, 'Hello');
// Sync with peers
const provider = new WebsocketProvider('ws://localhost:1234', 'room', ydoc);
Impact: Concurrent edits merge correctly, no data loss.
Problem: User goes offline, loses work or sees stale state.
Wrong approach:
// ❌ No offline handling
socket.on('disconnect', () => {
console.log('Disconnected'); // That's it?!
});
Why wrong: Pending changes lost, no reconnection strategy, bad UX.
Correct approach:
// ✅ Queue changes offline, sync on reconnect
const [isOnline, setIsOnline] = useState(true);
const [offlineQueue, setOfflineQueue] = useState<Change[]>([]);
socket.on('disconnect', () => {
setIsOnline(false);
showToast('Offline - changes will sync when reconnected');
});
socket.on('connect', () => {
setIsOnline(true);
// Send queued changes
if (offlineQueue.length > 0) {
socket.emit('sync-offline-changes', { changes: offlineQueue });
setOfflineQueue([]);
}
});
const handleChange = (change: Change) => {
if (isOnline) {
socket.emit('change', change);
} else {
setOfflineQueue(prev => [...prev, change]);
}
};
Timeline context:
Problem: No server authority, clients get out of sync.
Wrong approach:
// ❌ Clients broadcast to each other directly
socket.on('peer-change', ({ userId, change }) => {
applyChange(change); // No validation, no server state
});
Why wrong: Malicious client can send invalid data, no recovery from desync.
Correct approach:
// ✅ Server is source of truth
// Client
socket.emit('operation', { operation, clientRevision });
socket.on('ack', ({ serverRevision }) => {
if (serverRevision !== expectedRevision) {
// Desync detected, request full state
socket.emit('request-full-state');
}
});
// Server
io.on('connection', (socket) => {
socket.on('operation', ({ operation, clientRevision }) => {
// Validate operation
if (!isValid(operation)) {
socket.emit('error', { message: 'Invalid operation' });
return;
}
// Apply to server state
const serverRevision = applyOperation(operation);
// Broadcast to all clients
io.emit('operation', { operation, serverRevision });
});
});
Impact: Data integrity guaranteed, can recover from client bugs.
Problem: Users can't see who's editing what, causing edit conflicts.
Symptom: Two people editing same section unknowingly.
Wrong approach:
// ❌ No awareness of other users
function Editor() {
return <textarea />; // Flying blind!
}
Correct approach:
// ✅ Show active users and cursors
import { usePresence } from './usePresence';
function Editor() {
const { users, updateCursor } = usePresence();
const handleCursorMove = (position: number) => {
socket.emit('cursor-move', { userId: myId, position });
};
return (
<div>
{/* Show who's online */}
<UserList users={users} />
{/* Show remote cursors */}
<EditorWithCursors
content={content}
cursors={users.map(u => u.cursor)}
onCursorMove={handleCursorMove}
/>
</div>
);
}
Features:
import { io } from 'socket.io-client';
const socket = io('ws://localhost:3000', {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: Infinity,
transports: ['websocket', 'polling'] // Fallback
});
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
socket.on('disconnect', (reason) => {
if (reason === 'io server disconnect') {
// Server disconnected, manually reconnect
socket.connect();
}
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
});
import { TextOperation } from 'ot.js';
class OTEditor {
private revision = 0;
private pendingOperations: TextOperation[] = [];
applyLocalOperation(op: TextOperation): void {
// Apply immediately (optimistic update)
this.applyToEditor(op);
// Send to server
this.sendOperation(op);
// Store as pending
this.pendingOperations.push(op);
}
receiveRemoteOperation(op: TextOperation, serverRevision: number): void {
// Transform against pending operations
let transformed = op;
for (const pending of this.pendingOperations) {
[transformed, pending] = TextOperation.transform(transformed, pending);
}
// Apply transformed operation
this.applyToEditor(transformed);
this.revision = serverRevision;
}
acknowledgeOperation(serverRevision: number): void {
// Remove acknowledged operation from pending
this.pendingOperations.shift();
this.revision = serverRevision;
}
}
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
// Create shared document
const ydoc = new Y.Doc();
// Define shared types
const ytext = ydoc.getText('content');
const ymap = ydoc.getMap('metadata');
const yarray = ydoc.getArray('users');
// Connect to sync server
const provider = new WebsocketProvider(
'ws://localhost:1234',
'room-name',
ydoc
);
// Listen to changes
ytext.observe(event => {
console.log('Text changed:', event.changes);
});
// Make changes (automatically synced)
ytext.insert(0, 'Hello ');
ytext.insert(6, 'World!');
// Undo/redo support
const undoManager = new Y.UndoManager(ytext);
undoManager.undo();
undoManager.redo();
import { Awareness } from 'y-protocols/awareness';
const awareness = provider.awareness;
// Set local state
awareness.setLocalState({
user: {
name: 'Alice',
color: '#ff0000',
cursor: { line: 10, ch: 5 }
}
});
// Listen to changes
awareness.on('change', ({ added, updated, removed }) => {
// Update UI with user cursors/selections
const states = awareness.getStates();
states.forEach((state, clientId) => {
if (clientId !== awareness.clientID) {
renderCursor(state.user.cursor, state.user.color);
}
});
});
class OptimisticEditor {
private optimisticChanges = new Map<string, Change>();
async applyChange(change: Change): Promise<void> {
const changeId = generateId();
// Apply immediately (optimistic)
this.applyToUI(change);
this.optimisticChanges.set(changeId, change);
try {
// Send to server
const result = await this.sendToServer(change);
// Success - remove from optimistic
this.optimisticChanges.delete(changeId);
} catch (error) {
// Failed - rollback
this.rollback(changeId);
this.showError('Could not apply change');
}
}
private rollback(changeId: string): void {
const change = this.optimisticChanges.get(changeId);
if (change) {
this.revertInUI(change);
this.optimisticChanges.delete(changeId);
}
}
}
□ WebSocket connection with auto-reconnect
□ Offline queue for pending changes
□ Conflict resolution strategy (OT or CRDT)
□ Server authority (clients can't desync)
□ Presence awareness (cursors, active users)
□ Optimistic updates with rollback
□ Change batching (not per-keystroke)
□ Message compression for large payloads
□ Authentication and authorization
□ Rate limiting (prevent spam)
□ Heartbeat/ping-pong to detect dead connections
□ Graceful degradation (falls back to polling if WebSocket fails)
| Scenario | Strategy | |----------|----------| | Text editing (Google Docs) | ✅ Operational Transform | | JSON objects (Figma) | ✅ CRDTs (Yjs, Automerge) | | Simple cursor sharing | ✅ Basic WebSocket + presence | | Chat messages | ✅ Simple append-only (no OT/CRDT) | | Video timeline editing | ✅ CRDTs for timeline, OT for text | | Read-only dashboards | ❌ Use Server-Sent Events instead |
/references/ot-vs-crdt.md - Deep comparison of conflict resolution strategies/references/websocket-scaling.md - Scaling to millions of concurrent connections/references/presence-patterns.md - Cursor tracking, user awareness, activity indicatorsscripts/collaboration_tester.ts - Simulate concurrent edits, test conflict resolutionscripts/latency_simulator.ts - Test behavior under high latency/packet lossThis skill guides: Real-time collaboration | WebSocket architecture | Operational Transform | CRDTs | Presence awareness | Conflict resolution
tools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.