Claude Code Plugins

Community-maintained marketplace

Feedback

Builds real-time collaborative features with Liveblocks including presence, cursors, storage, comments, and notifications. Use when adding multiplayer experiences, collaborative editing, or live cursors to React applications.

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 liveblocks
description Builds real-time collaborative features with Liveblocks including presence, cursors, storage, comments, and notifications. Use when adding multiplayer experiences, collaborative editing, or live cursors to React applications.

Liveblocks

Complete toolkit for adding real-time collaboration to your app. Includes presence, storage, comments, notifications, and Yjs/Redux integration.

Quick Start

npm install @liveblocks/client @liveblocks/react

Configuration

// liveblocks.config.ts
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";

const client = createClient({
  publicApiKey: "pk_xxx",  // or authEndpoint for production
});

// Define your types
type Presence = {
  cursor: { x: number; y: number } | null;
  name: string;
};

type Storage = {
  todos: LiveList<{ id: string; text: string; done: boolean }>;
};

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

export const {
  RoomProvider,
  useMyPresence,
  useUpdateMyPresence,
  useOthers,
  useSelf,
  useStorage,
  useMutation,
  useRoom,
} = createRoomContext<Presence, Storage, UserMeta>(client);

Basic Usage

import { RoomProvider } from "./liveblocks.config";

function App() {
  return (
    <RoomProvider
      id="my-room"
      initialPresence={{ cursor: null, name: "Anonymous" }}
    >
      <CollaborativeApp />
    </RoomProvider>
  );
}

Presence

Track ephemeral user state (cursors, selections, typing indicators).

useMyPresence / useUpdateMyPresence

function Cursors() {
  const updateMyPresence = useUpdateMyPresence();

  return (
    <div
      onPointerMove={(e) => {
        updateMyPresence({
          cursor: { x: e.clientX, y: e.clientY }
        });
      }}
      onPointerLeave={() => {
        updateMyPresence({ cursor: null });
      }}
    >
      <OthersCursors />
    </div>
  );
}

useOthers

function OthersCursors() {
  const others = useOthers();

  return (
    <>
      {others.map(({ connectionId, presence, info }) => {
        if (!presence.cursor) return null;

        return (
          <Cursor
            key={connectionId}
            x={presence.cursor.x}
            y={presence.cursor.y}
            name={info?.name}
          />
        );
      })}
    </>
  );
}

useSelf

function UserInfo() {
  const self = useSelf();

  if (!self) return null;

  return (
    <div>
      <span>You: {self.info?.name}</span>
      <span>Connection: {self.connectionId}</span>
    </div>
  );
}

Selector Pattern (Performance)

// Only re-render when count changes
const count = useOthers((others) => others.length);

// Only get cursor positions
const cursors = useOthers((others) =>
  others.map((other) => ({
    id: other.connectionId,
    cursor: other.presence.cursor
  }))
);

Storage

Persist and sync data across clients using conflict-free data types.

Initialize Storage

<RoomProvider
  id="my-room"
  initialPresence={{ cursor: null }}
  initialStorage={{
    todos: new LiveList([]),
    canvas: new LiveMap(),
    settings: new LiveObject({ theme: "dark" })
  }}
>

useStorage

function TodoList() {
  const todos = useStorage((root) => root.todos);

  if (todos === null) {
    return <div>Loading...</div>;
  }

  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

useMutation

Modify storage safely with mutations.

function TodoList() {
  const todos = useStorage((root) => root.todos);

  const addTodo = useMutation(({ storage }, text: string) => {
    const todos = storage.get("todos");
    todos.push({
      id: crypto.randomUUID(),
      text,
      done: false
    });
  }, []);

  const toggleTodo = useMutation(({ storage }, index: number) => {
    const todos = storage.get("todos");
    const todo = todos.get(index);
    if (todo) {
      todo.done = !todo.done;
    }
  }, []);

  const deleteTodo = useMutation(({ storage }, index: number) => {
    storage.get("todos").delete(index);
  }, []);

  return (
    <div>
      <button onClick={() => addTodo("New task")}>Add</button>
      <ul>
        {todos?.map((todo, i) => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.done}
              onChange={() => toggleTodo(i)}
            />
            {todo.text}
            <button onClick={() => deleteTodo(i)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Live Data Types

LiveList

const addItem = useMutation(({ storage }) => {
  const list = storage.get("items");
  list.push({ id: "1", value: "new" });      // Add to end
  list.insert({ id: "2", value: "at 0" }, 0); // Insert at index
  list.move(0, 2);                            // Move item
  list.delete(1);                             // Delete at index
  list.clear();                               // Remove all
}, []);

LiveMap

const updateMap = useMutation(({ storage }) => {
  const map = storage.get("shapes");
  map.set("shape-1", { x: 100, y: 200 });    // Set value
  map.get("shape-1");                         // Get value
  map.delete("shape-1");                      // Delete key
  map.has("shape-1");                         // Check existence
}, []);

LiveObject

const updateObject = useMutation(({ storage }) => {
  const settings = storage.get("settings");
  settings.set("theme", "light");            // Set property
  settings.get("theme");                      // Get property
  settings.update({ theme: "dark", fontSize: 16 }); // Update multiple
}, []);

Suspense Support

import { ClientSideSuspense } from "@liveblocks/react";
import { useStorage } from "@liveblocks/react/suspense";

function App() {
  return (
    <RoomProvider id="room">
      <ClientSideSuspense fallback={<Loading />}>
        <Editor />
      </ClientSideSuspense>
    </RoomProvider>
  );
}

function Editor() {
  // No null check needed with suspense hooks
  const todos = useStorage((root) => root.todos);

  return <TodoList todos={todos} />;
}

Authentication

Auth Endpoint

// app/api/liveblocks-auth/route.ts
import { Liveblocks } from "@liveblocks/node";

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

export async function POST(request: Request) {
  const session = await getSession(); // Your auth

  const { room } = await request.json();

  const session = liveblocks.prepareSession(session.user.id, {
    userInfo: {
      name: session.user.name,
      avatar: session.user.avatar,
    },
  });

  // Grant access to room
  session.allow(room, session.FULL_ACCESS);

  const { body, status } = await session.authorize();
  return new Response(body, { status });
}

Client Config

const client = createClient({
  authEndpoint: "/api/liveblocks-auth",
});

Broadcast

Send transient messages without storing them.

function Chat() {
  const room = useRoom();

  const sendMessage = (text: string) => {
    room.broadcastEvent({
      type: "MESSAGE",
      text,
    });
  };

  useEffect(() => {
    return room.subscribe("event", ({ event }) => {
      if (event.type === "MESSAGE") {
        console.log("Received:", event.text);
      }
    });
  }, [room]);

  return <button onClick={() => sendMessage("Hello!")}>Send</button>;
}

History / Undo-Redo

import { useHistory, useCanUndo, useCanRedo } from "@liveblocks/react";

function UndoRedo() {
  const history = useHistory();
  const canUndo = useCanUndo();
  const canRedo = useCanRedo();

  return (
    <div>
      <button onClick={history.undo} disabled={!canUndo}>
        Undo
      </button>
      <button onClick={history.redo} disabled={!canRedo}>
        Redo
      </button>
    </div>
  );
}

// Batch changes for single undo step
const batchUpdate = useMutation(({ storage }) => {
  storage.get("todos").push({ id: "1", text: "A" });
  storage.get("todos").push({ id: "2", text: "B" });
  // Both will undo together
}, []);

// Pause/resume history
history.pause();
// ... make changes that shouldn't be in history
history.resume();

Yjs Integration

Use Liveblocks as a Yjs provider for text editors.

npm install @liveblocks/yjs yjs
import { useRoom } from "./liveblocks.config";
import { LiveblocksYjsProvider } from "@liveblocks/yjs";
import * as Y from "yjs";

function Editor() {
  const room = useRoom();

  useEffect(() => {
    const yDoc = new Y.Doc();
    const yProvider = new LiveblocksYjsProvider(room, yDoc);

    // Use with TipTap, Quill, Lexical, etc.
    const editor = new YourEditor({
      extensions: [
        Collaboration.configure({ document: yDoc }),
        CollaborationCursor.configure({ provider: yProvider }),
      ],
    });

    return () => {
      yProvider.destroy();
      yDoc.destroy();
    };
  }, [room]);
}

Comments

Add contextual comments to your app.

npm install @liveblocks/react-comments
import { Thread, Composer } from "@liveblocks/react-comments";
import { useThreads } from "@liveblocks/react/suspense";

function Comments() {
  const { threads } = useThreads();

  return (
    <div>
      {threads.map((thread) => (
        <Thread key={thread.id} thread={thread} />
      ))}
      <Composer />
    </div>
  );
}

Common Patterns

Multiplayer Cursors

function Cursors() {
  const updateMyPresence = useUpdateMyPresence();
  const others = useOthers((others) =>
    others.filter((o) => o.presence.cursor !== null)
  );

  return (
    <div
      className="canvas"
      onPointerMove={(e) => {
        const rect = e.currentTarget.getBoundingClientRect();
        updateMyPresence({
          cursor: {
            x: e.clientX - rect.left,
            y: e.clientY - rect.top,
          },
        });
      }}
      onPointerLeave={() => updateMyPresence({ cursor: null })}
    >
      {others.map(({ connectionId, presence, info }) => (
        <Cursor
          key={connectionId}
          x={presence.cursor!.x}
          y={presence.cursor!.y}
          name={info?.name}
        />
      ))}
    </div>
  );
}

function Cursor({ x, y, name }: { x: number; y: number; name?: string }) {
  return (
    <div
      className="cursor"
      style={{
        position: "absolute",
        left: x,
        top: y,
        pointerEvents: "none",
      }}
    >
      <svg>...</svg>
      {name && <span>{name}</span>}
    </div>
  );
}

Who's Here

function AvatarStack() {
  const others = useOthers();
  const self = useSelf();

  return (
    <div className="avatar-stack">
      {self && <Avatar user={self.info} />}
      {others.map(({ connectionId, info }) => (
        <Avatar key={connectionId} user={info} />
      ))}
    </div>
  );
}

Live Selection

type Presence = {
  selectedId: string | null;
};

function SelectableItems() {
  const updateMyPresence = useUpdateMyPresence();
  const othersSelections = useOthers((others) =>
    others.map((o) => o.presence.selectedId).filter(Boolean)
  );

  const items = useStorage((root) => root.items);

  return (
    <div>
      {items?.map((item) => (
        <div
          key={item.id}
          onClick={() => updateMyPresence({ selectedId: item.id })}
          className={othersSelections.includes(item.id) ? "selected-by-other" : ""}
        >
          {item.name}
        </div>
      ))}
    </div>
  );
}

Best Practices

  1. Use selectors to avoid unnecessary re-renders
  2. Type your presence/storage for autocomplete and safety
  3. Use Suspense hooks for cleaner loading states
  4. Batch mutations for undo/redo grouping
  5. Use authEndpoint in production (not public API key)
  6. Debounce cursor updates for performance