Claude Code for Liveblocks: Real-Time Collaboration in React — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Liveblocks: Real-Time Collaboration in React
Frontend

Claude Code for Liveblocks: Real-Time Collaboration in React

Published: March 10, 2027
Read time: 8 min read
By: Claude Skills 360

Liveblocks adds real-time collaboration to React apps — presence tracks who’s online with shared cursors and selections; storage syncs shared data with conflict-free CRDT guarantees. createClient({ authEndpoint }) authenticates with your backend. RoomProvider wraps the collaborative section. useMyPresence broadcasts the current user’s cursor position; useOthers renders all connected users’ presence. useStorage reads CRDT storage (LiveList, LiveObject, LiveMap); useMutation writes to it atomically. useHistory provides undo and redo for storage mutations. useThreads and useCreateThread add commenting to any document with persistent threads. Liveblocks Rooms are ephemeral by default but can persist storage. Claude Code generates Liveblocks client configuration, presence hooks with cursor rendering, storage mutations for collaborative editing, server-side auth endpoints, and the Next.js integration patterns.

CLAUDE.md for Liveblocks

## Liveblocks Stack
- Version: @liveblocks/react >= 2.0, @liveblocks/client >= 2.0
- Client: createClient({ authEndpoint: "/api/liveblocks-auth" }) — server validates users
- Room: RoomProvider + LiveblocksProvider in app tree
- Presence: useMyPresence([cursor]) — broadcast; useOthers() — read others
- Storage: useStorage(root => root.list) → LiveList; useMutation to write
- Types: defined in liveblocks.config.ts — Presence, Storage, UserMeta, RoomEvent
- History: useHistory() — history.undo() / history.redo() for storage mutations
- Threads: useThreads() + useCreateThread() for contextual comments

Configuration

// liveblocks.config.ts — type configuration
import { createClient, LiveList, LiveMap, LiveObject } from "@liveblocks/client"
import { createRoomContext } from "@liveblocks/react"

export const client = createClient({
  authEndpoint: "/api/liveblocks-auth",
  // Or public key for testing: publicApiKey: "pk_dev_xxxx"
  throttle: 16,  // 60fps cursor updates
})

export type Presence = {
  cursor: { x: number; y: number } | null
  selectedId: string | null
  name: string
  color: string
}

export type Storage = {
  // Collaborative document
  nodes: LiveMap<
    string,
    LiveObject<{
      id: string
      type: "text" | "image" | "shape"
      x: number
      y: number
      width: number
      height: number
      content?: string
    }>
  >
  order: LiveList<string>  // Z-order of node IDs
}

export type UserMeta = {
  id: string
  info: { name: string; avatar: string; color: string }
}

export type RoomEvent =
  | { type: "NOTIFICATION"; message: string }
  | { type: "CURSOR_LOCK"; nodeId: string; userId: string }

const {
  RoomProvider,
  useRoom,
  useMyPresence,
  useUpdateMyPresence,
  useOthers,
  useOthersMapped,
  useStorage,
  useMutation,
  useSelf,
  useHistory,
  useBatch,
  useStatus,
  useThreads,
  useCreateThread,
} = createRoomContext<Presence, Storage, UserMeta, RoomEvent>(client)

export {
  RoomProvider,
  useRoom,
  useMyPresence,
  useUpdateMyPresence,
  useOthers,
  useOthersMapped,
  useStorage,
  useMutation,
  useSelf,
  useHistory,
  useBatch,
  useStatus,
  useThreads,
  useCreateThread,
}

Auth Endpoint

// app/api/liveblocks-auth/route.ts — server auth
import { Liveblocks } from "@liveblocks/node"
import { auth } from "@/lib/auth"
import type { NextRequest } from "next/server"
import { NextResponse } from "next/server"

const liveblocks = new Liveblocks({
  secret: process.env.LIVEBLOCKS_SECRET_KEY!,
})

export async function POST(req: NextRequest) {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
  }

  const { room } = await req.json()

  // Validate the user can access this room
  const canAccess = await checkUserRoomAccess(session.user.id, room)
  if (!canAccess) {
    return NextResponse.json({ error: "Forbidden" }, { status: 403 })
  }

  const liveblocksSession = liveblocks.prepareSession(session.user.id, {
    userInfo: {
      name: session.user.name ?? "Anonymous",
      avatar: session.user.image ?? "",
      color: generateUserColor(session.user.id),
    },
  })

  liveblocksSession.allow(room, liveblocksSession.FULL_ACCESS)

  const { status, body } = await liveblocksSession.authorize()
  return new NextResponse(body, { status })
}

function generateUserColor(userId: string): string {
  const colors = ["#e03131", "#2f9e44", "#1971c2", "#f08c00", "#7048e8", "#0ca678"]
  const hash = userId.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0)
  return colors[hash % colors.length]
}

Presence — Cursors and Avatars

// components/canvas/CollaborativeCursors.tsx — live cursors
"use client"
import { useOthersMapped, useUpdateMyPresence } from "@/liveblocks.config"
import { useCallback, useEffect } from "react"

export function CollaborativeCursors({ canvasRef }: { canvasRef: React.RefObject<HTMLDivElement> }) {
  const updatePresence = useUpdateMyPresence()
  const others = useOthersMapped(other => ({
    cursor: other.presence.cursor,
    name: other.info?.name ?? "Anonymous",
    color: other.info?.color ?? "#888",
  }))

  const handlePointerMove = useCallback(
    (e: PointerEvent) => {
      if (!canvasRef.current) return
      const rect = canvasRef.current.getBoundingClientRect()
      updatePresence({
        cursor: {
          x: e.clientX - rect.left,
          y: e.clientY - rect.top,
        },
      })
    },
    [updatePresence, canvasRef]
  )

  const handlePointerLeave = useCallback(() => {
    updatePresence({ cursor: null })
  }, [updatePresence])

  useEffect(() => {
    const el = canvasRef.current
    if (!el) return
    el.addEventListener("pointermove", handlePointerMove)
    el.addEventListener("pointerleave", handlePointerLeave)
    return () => {
      el.removeEventListener("pointermove", handlePointerMove)
      el.removeEventListener("pointerleave", handlePointerLeave)
    }
  }, [handlePointerMove, handlePointerLeave, canvasRef])

  return (
    <>
      {others.map(([connectionId, { cursor, name, color }]) =>
        cursor ? (
          <div
            key={connectionId}
            className="absolute pointer-events-none z-50"
            style={{ left: cursor.x, top: cursor.y, transform: "translate(-2px, -2px)" }}
          >
            <svg width="18" height="22" fill="none">
              <path d="M0 0l6 18 3-5 5 3z" fill={color} />
            </svg>
            <div
              className="rounded px-1.5 py-0.5 text-xs text-white font-medium whitespace-nowrap mt-1"
              style={{ backgroundColor: color }}
            >
              {name}
            </div>
          </div>
        ) : null
      )}
    </>
  )
}

Storage Mutations

// components/canvas/useCanvasActions.ts — storage mutations
import { useMutation, useHistory } from "@/liveblocks.config"
import { LiveObject } from "@liveblocks/client"

export function useCanvasActions() {
  const { undo, redo, canUndo, canRedo, pause, resume } = useHistory()

  const addNode = useMutation(
    ({ storage }, node: { type: "text" | "image"; x: number; y: number }) => {
      const nodes = storage.get("nodes")
      const order = storage.get("order")
      const id = crypto.randomUUID()

      nodes.set(
        id,
        new LiveObject({
          id,
          type: node.type,
          x: node.x,
          y: node.y,
          width: 200,
          height: 100,
          content: node.type === "text" ? "Double-click to edit" : undefined,
        })
      )
      order.push(id)
    },
    []
  )

  const moveNode = useMutation(
    ({ storage }, { id, x, y }: { id: string; x: number; y: number }) => {
      const node = storage.get("nodes").get(id)
      node?.set("x", x)
      node?.set("y", y)
    },
    []
  )

  const deleteNode = useMutation(({ storage }, id: string) => {
    storage.get("nodes").delete(id)
    const order = storage.get("order")
    const idx = order.indexOf(id)
    if (idx !== -1) order.delete(idx)
  }, [])

  const updateNodeContent = useMutation(
    ({ storage }, { id, content }: { id: string; content: string }) => {
      storage.get("nodes").get(id)?.set("content", content)
    },
    []
  )

  return {
    addNode,
    moveNode,
    deleteNode,
    updateNodeContent,
    undo,
    redo,
    canUndo,
    canRedo,
  }
}

Online Avatars

// components/canvas/ActiveUsers.tsx — presence avatars
import { useSelf, useOthersMapped } from "@/liveblocks.config"

export function ActiveUsers() {
  const me = useSelf()
  const others = useOthersMapped(o => ({ name: o.info?.name, avatar: o.info?.avatar, color: o.info?.color }))

  return (
    <div className="flex -space-x-2">
      {[...others.entries()].slice(0, 4).map(([id, { name, avatar, color }]) => (
        <img
          key={id}
          src={avatar ?? `https://api.dicebear.com/9.x/initials/svg?seed=${name}`}
          alt={name ?? "User"}
          title={name}
          className="h-8 w-8 rounded-full border-2 border-background object-cover"
          style={{ borderColor: color }}
        />
      ))}
      {others.size > 4 && (
        <div className="h-8 w-8 rounded-full border-2 border-background bg-muted flex items-center justify-center text-xs font-medium">
          +{others.size - 4}
        </div>
      )}
    </div>
  )
}

For the Y.js real-time collaboration alternative that uses the same CRDT primitives (YArray, YMap, YText) directly with freedom to choose your own transport layer (WebSocket, WebRTC, or Hocuspocus) without Liveblocks’ authentication and presence infrastructure, see the Tiptap collaboration guide for Y.js + Hocuspocus patterns. For the Ably real-time messaging alternative when you need a lower-level pub/sub message channel model without the structured CRDT storage — better for custom real-time protocols and fan-out messaging patterns, the Supabase realtime guide covers channel subscription patterns. The Claude Skills 360 bundle includes Liveblocks skill sets covering presence, storage mutations, and auth. Start with the free tier to try collaborative feature generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

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