Skip to content
Back to skills

Liveblocks

ASecurity

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.

  • 4 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 10, 2026
businesstypescriptbashreactnodeapiperformance

Works with

  • cursor
  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 10, 2026

npx -y skills add mgd34msu/goodvibes-gemini --skill liveblocks --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Liveblocks?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Liveblocks
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mgd34msu-liveblocks/badge)](https://www.skillsdirectory.com/skills/mgd34msu-liveblocks)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
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

```bash
npm install @liveblocks/client @liveblocks/react
```

### Configuration

```typescript
// 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

```tsx
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

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

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

### useOthers

```tsx
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

```tsx
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)

```tsx
// 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

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

### useStorage

```tsx
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.

```tsx
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

```tsx
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

```tsx
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

```tsx
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

```tsx
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

```typescript
// 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

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

## Broadcast

Send transient messages without storing them.

```tsx
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

```tsx
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.

```bash
npm install @liveblocks/yjs yjs
```

```tsx
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.

```bash
npm install @liveblocks/react-comments
```

```tsx
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

```tsx
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

```tsx
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

```tsx
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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.