Claude Code Plugins

Community-maintained marketplace

Feedback

real-time-collaboration-patterns

@chriscarterux/chris-claude-stack
1
0

This skill should be used when implementing real-time collaborative features (multiplayer editing, live cursors, presence, chat) - covers WebSocket patterns, CRDTs, conflict resolution, operational transforms, sync strategies, and implementation with Supabase Realtime, Socket.io, or Partykit.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name real-time-collaboration-patterns
description This skill should be used when implementing real-time collaborative features (multiplayer editing, live cursors, presence, chat) - covers WebSocket patterns, CRDTs, conflict resolution, operational transforms, sync strategies, and implementation with Supabase Realtime, Socket.io, or Partykit.

Real-Time Collaboration Patterns

Overview

Build multiplayer experiences where multiple users interact simultaneously. This skill teaches conflict-free synchronization, presence awareness, and real-time data patterns.

Core principle: Optimistic updates + eventual consistency = smooth collaborative UX.

When to Use

Use when implementing:

  • Collaborative editing (docs, whiteboards, code)
  • Live presence (who's online, cursors)
  • Real-time chat
  • Multiplayer features
  • Live dashboards/monitoring
  • Collaborative tools

Technology Selection

Use Case Best Choice Why
Chat messages Supabase Realtime Simple, integrated with Postgres
Presence (cursors, status) PartyKit or Supabase Low latency, efficient
Collaborative editing Yjs + WebSockets CRDT for conflict-free edits
Live dashboards Server-Sent Events One-way data flow
Multiplayer games PartyKit or Socket.io Low latency, custom logic

WebSocket Basics

Client-Side (React)

import {useEffect, useState} from 'react'

function useWebSocket(url: string) {
  const [messages, setMessages] = useState<string[]>([])
  const [ws, setWs] = useState<WebSocket | null>(null)

  useEffect(() => {
    const socket = new WebSocket(url)

    socket.onopen = () => console.log('Connected')

    socket.onmessage = (event) => {
      setMessages(prev => [...prev, event.data])
    }

    socket.onerror = (error) => console.error('WebSocket error:', error)

    socket.onclose = () => console.log('Disconnected')

    setWs(socket)

    return () => socket.close()
  }, [url])

  const send = (message: string) => {
    ws?.send(message)
  }

  return {messages, send, connected: ws?.readyState === WebSocket.OPEN}
}

Server-Side (Node.js)

import {WebSocketServer} from 'ws'

const wss = new WebSocketServer({port: 8080})

const clients = new Set<WebSocket>()

wss.on('connection', (ws) => {
  clients.add(ws)

  ws.on('message', (data) => {
    // Broadcast to all clients
    clients.forEach(client => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data)
      }
    })
  })

  ws.on('close', () => {
    clients.delete(ws)
  })
})

Supabase Realtime (Easiest)

Setup

import {createClient} from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

Subscribe to Changes

// Listen to database changes
const channel = supabase
  .channel('messages')
  .on('postgres_changes', {
    event: 'INSERT',
    schema: 'public',
    table: 'messages'
  }, (payload) => {
    console.log('New message:', payload.new)
    setMessages(prev => [...prev, payload.new])
  })
  .subscribe()

// Clean up
return () => {
  supabase.removeChannel(channel)
}

Presence (Who's Online)

const channel = supabase.channel('room1')

// Track presence
channel
  .on('presence', {event: 'sync'}, () => {
    const state = channel.presenceState()
    setOnlineUsers(Object.values(state).flat())
  })
  .subscribe(async (status) => {
    if (status === 'SUBSCRIBED') {
      await channel.track({
        user: currentUser.id,
        online_at: new Date().toISOString()
      })
    }
  })

Broadcast (Real-time Events)

// Send events
channel.send({
  type: 'broadcast',
  event: 'cursor_move',
  payload: {x: 100, y: 200, user: userId}
})

// Receive events
channel
  .on('broadcast', {event: 'cursor_move'}, (payload) => {
    updateCursor(payload.user, payload.x, payload.y)
  })
  .subscribe()

Conflict Resolution Strategies

Last Write Wins (Simple)

Use for:

  • Non-critical data
  • Single field updates
  • Chat messages
  • Notifications

Pattern:

// Timestamp determines winner
const latest = updates.reduce((latest, update) =>
  update.timestamp > latest.timestamp ? update : latest
)

Operational Transforms (Complex)

Use for:

  • Text editing
  • When order of operations matters

Libraries:

  • ShareDB
  • Yjs

CRDTs (Conflict-Free Replicated Data Types)

Use for:

  • Collaborative editing
  • Offline-first apps
  • Complex data structures

Libraries:

  • Yjs (most popular)
  • Automerge

Example (Yjs):

import * as Y from 'yjs'
import {WebsocketProvider} from 'y-websocket'

const doc = new Y.Doc()
const provider = new WebsocketProvider('ws://localhost:1234', 'room1', doc)

const yText = doc.getText('shared-text')

// Listen to changes
yText.observe(() => {
  setContent(yText.toString())
})

// Make changes
yText.insert(0, 'Hello ')

Common Patterns

Pattern 1: Live Cursors

interface Cursor {
  userId: string
  x: number
  y: number
  color: string
}

function LiveCursors() {
  const [cursors, setCursors] = useState<Map<string, Cursor>>(new Map())

  useEffect(() => {
    const channel = supabase.channel('cursors')

    channel
      .on('broadcast', {event: 'cursor'}, ({payload}) => {
        setCursors(prev => new Map(prev).set(payload.userId, payload))
      })
      .subscribe()

    // Track own cursor
    const handleMouseMove = (e: MouseEvent) => {
      channel.send({
        type: 'broadcast',
        event: 'cursor',
        payload: {
          userId: currentUser.id,
          x: e.clientX,
          y: e.clientY,
          color: currentUser.color
        }
      })
    }

    window.addEventListener('mousemove', handleMouseMove)
    return () => window.removeEventListener('mousemove', handleMouseMove)
  }, [])

  return (
    <>
      {Array.from(cursors.values()).map(cursor => (
        <Cursor key={cursor.userId} {...cursor} />
      ))}
    </>
  )
}

Pattern 2: Optimistic Updates

async function sendMessage(text: string) {
  const tempId = crypto.randomUUID()

  // Optimistic: Show immediately
  setMessages(prev => [...prev, {
    id: tempId,
    text,
    pending: true
  }])

  try {
    // Actually send to server
    const {data} = await supabase
      .from('messages')
      .insert({text})
      .select()
      .single()

    // Replace temp with real
    setMessages(prev =>
      prev.map(m => m.id === tempId ? data : m)
    )
  } catch (error) {
    // Rollback on error
    setMessages(prev => prev.filter(m => m.id !== tempId))
    showError('Failed to send message')
  }
}

Pattern 3: Presence Indicators

function PresenceList() {
  const [online, setOnline] = useState<User[]>([])

  useEffect(() => {
    const channel = supabase.channel('presence')

    channel
      .on('presence', {event: 'sync'}, () => {
        const state = channel.presenceState()
        setOnline(Object.values(state).flat())
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          await channel.track({
            userId: currentUser.id,
            name: currentUser.name,
            avatar: currentUser.avatar
          })
        }
      })

    return () => supabase.removeChannel(channel)
  }, [])

  return (
    <div>
      {online.map(user => (
        <Avatar key={user.userId} user={user} online={true} />
      ))}
    </div>
  )
}

Performance Considerations

Throttle updates:

import {throttle} from 'lodash'

const sendCursorUpdate = throttle((x, y) => {
  channel.send({type: 'cursor', x, y})
}, 50) // Update max once per 50ms

Batch operations:

// Instead of sending each keystroke
yText.insert(position, character)

// Batch multiple changes
const changes = ['H', 'e', 'l', 'l', 'o']
yText.insert(position, changes.join(''))

Clean up inactive connections:

// Remove users offline >30s
const TIMEOUT = 30000

setInterval(() => {
  const now = Date.now()
  presences.forEach((presence, userId) => {
    if (now - presence.lastSeen > TIMEOUT) {
      presences.delete(userId)
    }
  })
}, 10000)

Resources

  • Supabase Realtime: supabase.com/docs/guides/realtime
  • Yjs (CRDT): yjs.dev
  • PartyKit: partykit.io
  • Socket.io: socket.io

Real-time collaboration makes apps feel magical. Start simple (Supabase), add complexity only when needed (Yjs/CRDTs).