# Liveblocks Documentation

Source: https://liveblocks.io/llms-full.txt

---

---
meta:
  title: "@liveblocks/client"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/client package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/client` provides you with JavaScript bindings for our realtime
collaboration APIs, built on top of WebSockets. Read our
[getting started](/docs/get-started) guides to learn more.

## createClient

Creates a [client](#Client) that allows you to connect to Liveblocks servers.
You must define either `authEndpoint` or `publicApiKey`. Resolver functions
should be placed inside here, and a number of other options are available.

```tsx
import { createClient } from "@liveblocks/client";

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

  // Other options
  // ...
});
```

```tsx title="Every createClient option" isCollapsable isCollapsed
import { createClient } from "@liveblocks/client";

const client = createClient({
  // Connect with authEndpoint
  authEndpoint: "/api/liveblocks-auth",

  // Alternatively, use an authEndpoint callback
  // authEndpoint: async (room) => {
  //   const response = await fetch("/api/liveblocks-auth", {
  //   method: "POST",
  //   headers: {
  //     Authentication: "<your own headers here>",
  //     "Content-Type": "application/json",
  //   },
  //   body: JSON.stringify({ room }),
  // });
  // return await response.json();
  // },

  // Alternatively, use a public key
  // publicApiKey: "pk_...",

  // Throttle time (ms) between WebSocket updates
  throttle: 100,

  // Prevent browser tab from closing while local changes aren’t synchronized yet
  preventUnsavedChanges: false,

  // Throw lost-connection event after 5 seconds offline
  lostConnectionTimeout: 5000,

  // Disconnect users after X (ms) of inactivity, disabled by default
  backgroundKeepAliveTimeout: undefined,

  // Resolve user info for Comments, Text Editor, and Notifications
  resolveUsers: async ({ userIds }) => {
    const usersData = await __getUsersFromDB__(userIds);

    return usersData.map((userData) => ({
      name: userData.name,
      avatar: userData.avatar.src,
    }));
  },

  // Resolve room info for Notifications
  resolveRoomsInfo: async ({ roomIds }) => {
    const documentsData = await __getDocumentsFromDB__(roomIds);

    return documentsData.map((documentData) => ({
      name: documentData.name,
      // url: documentData.url,
    }));
  },

  // Resolve group info for Comments and Text Editor
  resolveGroupsInfo: async ({ groupIds }) => {
    const groupsData = await __getGroupsFromDB__(groupIds);

    return groupsData.map((groupData) => ({
      avatar: groupData.avatar.src,
      name: groupData.name,
      // description: groupData.description,
    }));
  },

  // Resolve mention suggestions for Comments and Text Editor
  resolveMentionSuggestions: async ({ text, roomId }) => {
    const workspaceUsers = await __getWorkspaceUsersFromDB__(roomId);

    if (!text) {
      // Show all workspace users by default
      return __getUserIds__(workspaceUsers);
    } else {
      const matchingUsers = __findUsers__(workspaceUsers, text);
      return __getUserIds__(matchingUsers);
    }
  },

  // Polyfill options for non-browser environments
  polyfills: {
    // atob,
    // fetch,
    // WebSocket,
  },

  // Set the location of the "Powered by Liveblocks" badge
  // "top-right", "bottom-right", "bottom-left", "top-left"
  badgeLocation: "bottom-right",
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="client" type="Client">
    Returns a [Client](#Client), used for connecting to Liveblocks.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem
    name="authEndpoint"
    detailedType="string | async ((room?: string) => CustomAuthenticationResult)"
  >
    The URL of your back end’s [authentication endpoint](/docs/authentication)
    as a string, or an async callback function that returns a Liveblocks token
    result. Either `authEndpoint` or `publicApiKey` are required. Learn more
    about [using a URL string](#createClientAuthEndpoint) and [using a
    callback](#createClientCallback).
  </PropertiesListItem>
  <PropertiesListItem name="publicApiKey" type="string">
    The public API key taken from your project’s
    [dashboard](/dashboard/apikeys). Generally not recommended for production
    use. Either `authEndpoint` or `publicApiKey` are required. [Learn
    more](#createClientPublicKey).
  </PropertiesListItem>
  <PropertiesListItem name="throttle" type="number" defaultValue="100">
    The throttle time between WebSocket messages in milliseconds, a number
    between `16` and `1000` is allowed. Using `16` means your app will update 60
    times per second. [Learn more](#createClientThrottle).
  </PropertiesListItem>
  <PropertiesListItem
    name="preventUnsavedChanges"
    type="boolean"
    defaultValue="false"
  >
    When set, navigating away from the current page is prevented while
    Liveblocks is still synchronizing local changes. [Learn
    more](#prevent-users-losing-unsaved-changes).
  </PropertiesListItem>
  <PropertiesListItem
    name="lostConnectionTimeout"
    type="number"
    defaultValue="5000"
  >
    After a user disconnects, the time in milliseconds before a
    [`"lost-connection"`](/docs/api-reference/liveblocks-client#Room.subscribe.lost-connection)
    event is fired. [Learn more](#createClientLostConnectionTimeout).
  </PropertiesListItem>
  <PropertiesListItem name="backgroundKeepAliveTimeout" type="number">
    The time before an inactive WebSocket connection is disconnected. This is
    disabled by default, but setting a number will activate it. [Learn
    more](#createClientBackgroundKeepAliveTimeout).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveUsers"
    detailedType='async? (args: ResolveUsersArgs) => (UserMeta["info"] | undefined)[] | undefined'
  >
    A function that resolves user information in
    [Comments](/docs/ready-made-features/comments), [Text Editor](/docs/ready-made-features/text-editor), and [Notifications](/docs/ready-made-features/notifications). Return an array of
    `UserMeta["info"]` objects in the same order they arrived. [Learn
    more](#createClientResolveUsers).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveRoomsInfo"
    detailedType="async? (args: ResolveRoomsInfoArgs) => (RoomInfo | undefined)[] | undefined"
  >
    A function that resolves room information in
    [Notifications](/docs/ready-made-features/notifications). Return an array of
    `RoomInfo` objects in the same order they arrived. [Learn
    more](#createClientResolveRoomsInfo).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveGroupsInfo"
    detailedType="async? (args: ResolveGroupsInfoArgs) => (GroupInfo | undefined)[] | undefined"
  >
    A function that resolves group information in
    [Comments](/docs/ready-made-features/comments) and [Text Editor](/docs/ready-made-features/text-editor). Return an array of
    `GroupInfo` objects in the same order they arrived. [Learn
    more](#createClientResolveGroupsInfo).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveMentionSuggestions"
    detailedType="async? (args: ResolveMentionSuggestionsArgs) => string[] | MentionData[]"
  >
    A function that resolves mention suggestions in
    [Comments](/docs/ready-made-features/comments) and [Text Editor](/docs/ready-made-features/text-editor). Return an array of user IDs or mention objects.
    [Learn more](#createClientResolveMentionSuggestions).
  </PropertiesListItem>
  <PropertiesListItem name="polyfills">
    Place polyfills for `atob`, `fetch`, and `WebSocket` inside here. Useful
    when using a non-browser environment, such as [Node.js](#createClientNode)
    or [React Native](#createClientReactNative).
  </PropertiesListItem>
  <PropertiesListItem
    name="largeMessageStrategy"
    detailedType='"default" | "split" | "experimental-fallback-to-http"'
    defaultValue={`"default"`}
  >
    <div className="-mb-3">
      How to handle WebSocket messages that are larger than the maximum message size. Can be set to one of these values:
    </div>

    - `"default"` Don’t send anything, but log the error to the console and notify useErrorListener.
    - `"split"` Break the message up into chunks each of which is smaller than the maximum message size. Beware that using `"split"` will sacrifice atomicity of changes! Depending on your use case, this may or may not be problematic.
    - `"experimental-fallback-to-http"` Try sending the update over HTTP instead of WebSockets. This strategy is marked experimental, as it can make the client appear as "synchronizing" indefinitely when used with Liveblocks Storage or Yjs.

  </PropertiesListItem>
  <PropertiesListItem
    name="badgeLocation"
    detailedType='"top-right" | "bottom-right" | "bottom-left" | "top-left"'
    defaultValue={`"bottom-right"`}
  >
    The location of the "Powered by Liveblocks" badge. Can be set to either `"top-right"`, `"bottom-right"`, `"bottom-left"`, or `"top-left"`.
    [Learn more](#createClientBadgeLocation).
  </PropertiesListItem>
  <PropertiesListItem
    name="unstable_streamData"
    type="boolean"
    defaultValue="false"
  >
    Experimental. Stream the initial Storage content over HTTP, instead of
    waiting for a large initial WebSocket message to be sent from the server.
  </PropertiesListItem>
</PropertiesList>

### createClient with public key [#createClientPublicKey]

When creating a client with a public key, you don’t need to set up an
authorization endpoint. We only recommend using a public key when prototyping,
or on public landing pages, as it makes it possible for end users to access any
room’s data. You should instead use an
[auth endpoint](#createClientAuthEndpoint).

```ts
import { createClient } from "@liveblocks/client";

const client = createClient({
  publicApiKey: "{{PUBLIC_KEY}}",
});
```

### createClient with auth endpoint [#createClientAuthEndpoint]

If you are not using a public key, you need to set up your own `authEndpoint`.
Please refer to our [Authentication guide](/docs/authentication).

```ts
import { createClient } from "@liveblocks/client";

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

### createClient with auth endpoint callback [#createClientCallback]

If you need to add additional headers or use your own function to call your
endpoint, `authEndpoint` can be provided as a custom callback. You should return
the token created with
[`Liveblocks.prepareSession`](/docs/api-reference/liveblocks-node#access-tokens)
or [`liveblocks.identifyUser`](/docs/api-reference/liveblocks-node#id-tokens),
learn more in [authentication guide](/docs/rooms/authentication).

```ts
import { createClient } from "@liveblocks/client";

const client = createClient({
  authEndpoint: async (room) => {
    // Fetch your authentication endpoint and retrieve your access or ID token
    // ...

    return { token: "..." };
  },
});
```

`room` is the room ID that the user is connecting to. When using
[Notifications](/docs/ready-made-features/comments/email-notifications), `room`
can be `undefined`, as the client is requesting a token that grants access to
multiple rooms, rather than a specific room.

#### Fetch your endpoint

Here’s an example of fetching your API endpoint at `/api/liveblocks-auth` within
the callback.

```ts
import { createClient } from "@liveblocks/client";

const client = createClient({
  authEndpoint: async (room) => {
    const response = await fetch("/api/liveblocks-auth", {
      method: "POST",
      headers: {
        Authentication: "<your own headers here>",
        "Content-Type": "application/json",
      },
      // Don’t forget to pass `room` down. Note that it
      // can be undefined when using Notifications.
      body: JSON.stringify({ room }),
    });
    return await response.json();
  },
});
```

#### Token details

You should return the token created with
[`Liveblocks.prepareSession`](/docs/api-reference/liveblocks-node#access-tokens)
or [`liveblocks.identifyUser`](/docs/api-reference/liveblocks-node#id-tokens).
These are the values the functions can return.

1. A valid token, it returns a `{ "token": "..." }` shaped response.
1. A token that explicitly forbids access, it returns an
   `{ "error": "forbidden", "reason": "..." }` shaped response. If this is
   returned, the client will disconnect and won’t keep trying to authorize.

Any other error will be treated as an unexpected error, after which the client
will retry the request until it receives either 1. or 2.

### WebSocket throttle [#createClientThrottle]

By default, the client throttles the WebSocket messages sent to one every 100
milliseconds, which translates to 10 updates per second. It’s possible to
override that configuration with the `throttle` option with a value between `16`
and `1000` milliseconds.

```ts
import { createClient } from "@liveblocks/client";

const client = createClient({
  throttle: 16,

  // Other options
  // ...
});
```

This option is helpful for smoothing out realtime animations in your
application, as you can effectively increase the framerate without using any
interpolation. Here are some examples with their approximate frames per second
(FPS) values.

```ts
throttle:  16, // 60 FPS
throttle:  32, // 30 FPS
throttle: 200, //  5 FPS
```

### Prevent users losing unsaved changes [#prevent-users-losing-unsaved-changes]

Liveblocks usually synchronizes milliseconds after a local change, but if a user
immediately closes their tab, or if they have a slow connection, it may take
longer for changes to synchronize. Enabling `preventUnsavedChanges` will stop
tabs with unsaved changes closing, by opening a dialog that warns users. In
usual circumstances, it will very rarely trigger.

```tsx
import { createClient } from "@liveblocks/client";

const client = createClient({
  preventUnsavedChanges: true,

  // Other options
  // ...
});
```

More specifically, this option triggers when:

- There are unsaved changes after calling any hooks or methods, in all of our
  products.
- There are unsaved changes in a
  [Text Editor](/docs/ready-made-features/text-editor).
- There’s an unsubmitted comment in the
  [Composer](/docs/api-reference/liveblocks-react-ui#Composer).
- The user has made changes and is currently offline.

Internally, this option uses the
[beforeunload event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event).

### Lost connection timeout [#createClientLostConnectionTimeout]

If you’re connected to a room and briefly lose connection, Liveblocks will
reconnect automatically and quickly. However, if reconnecting takes longer than
usual, for example if your network is offline, then the room will emit an event
informing you about this.

How quickly this event is triggered can be configured with the
`lostConnectionTimeout` setting, and it takes a number in milliseconds.
`lostConnectionTimeout` can be set between `1000` and `30000` milliseconds. The
default is `5000`, or 5 seconds.

```ts
import { createClient } from "@liveblocks/client";

const client = createClient({
  lostConnectionTimeout: 5000,

  // Other options
  // ...
});
```

You can listen to the event with [`room.subscribe("lost-connection")`][]. Note
that this also affects when `others` are reset to an empty array after a
disconnection. This helps prevent temporary flashes in your application as a
user quickly disconnects and reconnects. For a demonstration of this behavior,
see our [connection status example][].

### Background keep-alive timeout [#createClientBackgroundKeepAliveTimeout]

By default, Liveblocks applications will maintain an active WebSocket connection
to the Liveblocks servers, even when running in a browser tab that’s in the
background. However, if you’d prefer for background tabs to disconnect after a
period of inactivity, then you can use `backgroundKeepAliveTimeout`.

When `backgroundKeepAliveTimeout` is specified, the client will automatically
disconnect applications that have been in an unfocused background tab for _at
least_ the specified time. When the browser tab is refocused, the client will
immediately reconnect to the room and synchronize the document.

```ts
import { createClient } from "@liveblocks/client";

const client = createClient({
  // Disconnect users after 15 minutes of inactivity
  backgroundKeepAliveTimeout: 15 * 60 * 1000,

  // Other options
  // ...
});
```

`backgroundKeepAliveTimeout` accepts a number in milliseconds—we advise using a
value of at least a few minutes, to avoid unnecessary disconnections.

### resolveUsers [#createClientResolveUsers]

[Comments](/docs/ready-made-features/comments) and
[Text Editor](/docs/ready-made-features/text-editor) store user IDs in their
system, but no other user information. To display user information in Comments,
Text Editor, and Notifications components, such as a user’s name or avatar, you
need to resolve these IDs into user objects. This function receives a list of
user IDs and you should return a list of user objects of the same size, in the
same order.

User IDs are automatically resolved in batches with a maximum of 50 users per
batch to optimize performance and prevent overwhelming your user resolution
function.

```tsx
import { createClient } from "@liveblocks/client";

const client = createClient({
  resolveUsers: async ({ userIds }) => {
    const usersData = await __getUsersFromDB__(userIds);

    return usersData.map((userData) => ({
      name: userData.name,
      avatar: userData.avatar.src,
    }));
  },

  // Other options
  // ...
});
```

The name and avatar you return are rendered in
[`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) components.

#### User objects

The user objects returned by the resolver function take the shape of
`UserMeta["info"]`, which contains `name` and `avatar` by default. These two
values are optional, though if you’re using the
[Comments default components](/docs/api-reference/liveblocks-react-ui#Components),
they are necessary. Here’s an example of `userIds` and the exact values
returned.

```ts
resolveUsers: async ({ userIds }) => {
  // ["marc@example.com", "nimesh@example.com"];
  console.log(userIds);

  return [
    { name: "Marc", avatar: "https://example.com/marc.png" },
    { name: "Nimesh", avatar: "https://example.com/nimesh.png" },
  ];
};
```

You can also return custom information, for example, a user’s `color`:

```ts
resolveUsers: async ({ userIds }) => {
  // ["marc@example.com"];
  console.log(userIds);

  return [
    {
      name: "Marc",
      avatar: "https://example.com/marc.png",
      // +++
      color: "purple",
      // +++
    },
  ];
};
```

#### Accessing user data in React

You can access any values set within `resolveUsers` with the
[`useUser`](/docs/api-reference/liveblocks-react#useUser) hook.

```tsx
import { useUser } from "@liveblocks/react/suspense";

function Component() {
  const user = useUser("marc@example.com");

  // { name: "Marc", avatar: "https://...", ... }
  console.log(user);
}
```

### resolveRoomsInfo [#createClientResolveRoomsInfo]

When using
[Notifications](/docs/ready-made-features/comments/email-notifications) with
[Comments](/docs/ready-made-features/comments), room IDs will be used to
contextualize notifications (e.g. “Chris mentioned you in _room-id_”) in the
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification)
component. To replace room IDs with more fitting names (e.g. document names,
“Chris mentioned you in _Document A_”), you can provide a resolver function to
the `resolveRoomsInfo` option in [`createClient`](#createClient).

This resolver function will receive a list of room IDs and should return a list
of room info objects of the same size and in the same order.

```tsx
import { createClient } from "@liveblocks/client";

const client = createClient({
  resolveRoomsInfo: async ({ roomIds }) => {
    const documentsData = await __getDocumentsFromDB__(roomIds);

    return documentsData.map((documentData) => ({
      name: documentData.name,
      // url: documentData.url,
    }));
  },

  // Other options
  // ...
});
```

In addition to the room’s name, you can also provide a room’s URL as the `url`
property. If you do so, the
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification)
component will automatically use it. It’s possible to use an inbox
notification’s `roomId` property to construct a room’s URL directly in React and
set it on
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification)
via `href`, but the room ID might not be enough for you to construct the URL,
you might need to call your backend for example. In that case, providing it via
`resolveRoomsInfo` is the preferred way.

### resolveGroupsInfo [#createClientResolveGroupsInfo]

When using group mentions with [Comments](/docs/ready-made-features/comments)
and [Text Editor](/docs/ready-made-features/text-editor), group IDs will be used
instead of user IDs. Similarly to [`resolveUsers`](#createClientResolveUsers),
you can provide a resolver function to the `resolveGroupsInfo` option in
[`createClient`](#createClient) to assign information like names and avatars to
group IDs.

```tsx
import { createClient } from "@liveblocks/client";

const client = createClient({
  resolveGroupsInfo: async ({ groupIds }) => {
    const groupsData = await __getGroupsFromDB__(groupIds);

    return groupsData.map((groupData) => ({
      name: groupData.name,
      avatar: groupData.avatar.src,
    }));
  },

  // Other options
  // ...
});
```

#### Accessing group info in React

You can access any values set within `resolveGroupsInfo` with the
[`useGroupInfo`](/docs/api-reference/liveblocks-react#useGroupInfo) hook.

```tsx
import { useGroupInfo } from "@liveblocks/react/suspense";

function Component() {
  const group = useGroupInfo("group-engineering");

  // { name: "Engineering", avatar: "https://...", ... }
  console.log(group);
}
```

### resolveMentionSuggestions [#createClientResolveMentionSuggestions]

To enable creating mentions in [Comments](/docs/ready-made-features/comments)
and [Text Editor](/docs/ready-made-features/text-editor), you can provide a
resolver function to the `resolveMentionSuggestions` option in
[`createClient`](#createClient). These mentions will be displayed in the
[`Composer`](/docs/api-reference/liveblocks-react-ui#Composer) component and in
text editors.

This resolver function will receive the mention currently being typed (e.g. when
writing “@jane”, `text` will be `jane`) and should return a list of user IDs
matching that text. This function will be called every time the text changes but
with some debouncing.

```tsx
import { createClient } from "@liveblocks/client";

const client = createClient({
  resolveMentionSuggestions: async ({ text, roomId }) => {
    const workspaceUsers = await __getWorkspaceUsersFromDB__(roomId);

    if (!text) {
      // Show all workspace users by default
      return __getUserIds__(workspaceUsers);
    } else {
      const matchingUsers = __findUsers__(workspaceUsers, text);
      return __getUserIds__(matchingUsers);
    }
  },

  // Other options
  // ...
});
```

#### Group mentions

To support group mentions in [Comments](/docs/ready-made-features/comments) and
[Text Editor](/docs/ready-made-features/text-editor), you can return a list of
mention objects instead of user IDs to suggest a mix of user and group mentions.

```tsx
import { createClient } from "@liveblocks/client";

const client = createClient({
  resolveMentionSuggestions: async ({ text, roomId }) => {
    const dbUsers = await __findUsersFromDB__(roomId);
    const dbGroups = await __findGroupsFromDB__(roomId);

    // Show groups and users matching the text being typed
    return [
      ...dbGroups.map((group) => ({
        kind: "group",
        id: group.id,
      })),
      ...dbUsers.map((user) => ({
        kind: "user",
        id: user.id,
      })),
    ];
  },

  // Other options
  // ...
});
```

The mention objects specify which kind of mention it is, the ID to mention (user
ID or group ID), etc.

```tsx
// A user mention suggestion
{
  kind: "user",
  id: "user-1",
}

// A group mention suggestion
{
  kind: "group",
  id: "group-1",
}

// A group mention suggestion with fixed group members
// When using fixed group members via `userIds`, they will take precedence
// if the group ID exists on Liveblocks.
{
  kind: "group",
  id: "here",
  members: ["user-1", "user-2"],
}
```

### createClient for Node.js [#createClientNode]

To use `@liveblocks/client` in Node.js, you need to provide [`WebSocket`][] and
[`fetch`][] polyfills. As polyfills, we recommend installing [`ws`][] and
[`node-fetch`][].

```bash
npm install ws node-fetch
```

Then, pass them to the `createClient` polyfill option as below.

```ts
import { createClient } from "@liveblocks/client";
import fetch from "node-fetch";
import WebSocket from "ws";

const client = createClient({
  polyfills: {
    fetch,
    WebSocket,
  },

  // Other options
  // ...
});
```

Note that `node-fetch` v3+
[does not support CommonJS](https://github.com/node-fetch/node-fetch/blob/main/docs/v3-UPGRADE-GUIDE.md#converted-to-es-module).
If you are using CommonJS, downgrade `node-fetch` to v2.

### createClient for React Native [#createClientReactNative]

To use `@liveblocks/client` with [React Native](https://reactnative.dev/), you
need to add an [`atob`][] polyfill. As a polyfill, we recommend installing
[`base-64`][].

```bash
npm install base-64
```

Then you can pass the `decode` function to our `atob` polyfill option when you
create the client.

```ts
import { createClient } from "@liveblocks/client";
import { decode } from "base-64";

const client = createClient({
  polyfills: {
    atob: decode,
  },

  // Other options
  // ...
});
```

### Powered by Liveblocks branding [#createClientBadgeLocation]

By default, Liveblocks displays a "Powered by Liveblocks" badge in your
application. You can adjust the position of the badge by setting the
`badgeLocation` property on `createClient`.

```ts title="Set badge location"
import { createClient } from "@liveblocks/client";

// "top-right", "bottom-right", "bottom-left", "top-left"
const client = createClient({
  badgeLocation: "bottom-right",

  // ...
});
```

If you wish to remove the badge entirely, you can do so by following these
steps:

1. In the Liveblocks dashboard, navigate to your
   [team's settings](/dashboard/settings).
2. Under **General**, toggle on the remove "Powered by Liveblocks" branding
   option.

<Banner title='Removing the "Powered by Liveblocks" badge'>

Removing the "Powered by Liveblocks" badge on your projects requires a
[paid plan](/pricing/). See the [pricing page](/pricing/) for more information.

</Banner>

## Client

Client returned by [`createClient`][] which allows you to connect to Liveblocks
servers in your application, and enter rooms.

### Client.enterRoom

Enters a room and returns both the local `Room` instance, and a `leave`
unsubscribe function. The authentication endpoint is called as soon as you call
this function. Used for setting [initial Presence](#setting-initial-presence)
and [initial Storage](#setting-initial-storage) values.

```ts
const { room, leave } = client.enterRoom("my-room-id", {
  // Options
  // ...
});
```

Note that it’s possible to [add types to your room](#typing-your-data).

<PropertiesList title="Returns">
  <PropertiesListItem
    name="room"
    type="Room<Presence, Storage, UserMeta, RoomEvent>"
  >
    A [Room](#Room), used for building your Liveblocks application. Learn more
    about [typing your room](#typing-your-data).
  </PropertiesListItem>
  <PropertiesListItem name="leave" type="() => void">
    A function that’s used to leave the room and disconnect.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="roomId" type="string" required>
    The ID of the room you’re connecting to.
  </PropertiesListItem>
  <PropertiesListItem name="options.initialPresence" type="JsonObject">
    The initial Presence of the user entering the room. Each user has their own
    presence, and this is readable for all other connected users. A user’s
    Presence resets every time they disconnect. This object must be
    JSON-serializable. [Learn more](#setting-initial-presence).
  </PropertiesListItem>
  <PropertiesListItem name="options.initialStorage" type="LsonObject">
    The initial Storage structure for the room when it’s joined for the first
    time. This is only set a single time, when the room has not yet been
    populated. This object must contain [conflict-free live
    structures](/docs/api-reference/liveblocks-client#Storage). [Learn
    more](#setting-initial-storage).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.autoConnect"
    type="boolean"
    defaultValue="true"
  >
    Whether the room immediately connects to Liveblocks servers.
  </PropertiesListItem>
</PropertiesList>

#### Setting initial Presence [#setting-initial-presence]

Presence is used for storing temporary user-based values, such as a user’s
cursor coordinates, or their current selection. Each user has their own
presence, and this is readable for all other connected users. Set your initial
Presence value by using `initialPresence`.

```ts
const { room, leave } = client.enterRoom("my-room-id", {
  // +++
  initialPresence: {
    cursor: null,
    colors: ["red", "purple"],
    selection: {
      id: 72426,
    },
  },
  // +++

  // Other options
  // ...
});
```

Each user’s Presence resets every time they disconnect, as this is only meant
for temporary data. Any JSON-serializable object is allowed (the `JsonObject`
type).

#### Setting initial Storage [#setting-initial-storage]

Storage is used to store permanent data that’s used in your application, such as
shapes on a whiteboard, nodes on a flowchart, or text in a form. The first time
a room is entered, you can set an initial value by using `initialStorage`.
`initialStorage` is only read and set a single time, unless a new top-level
property is added.

```ts
import { LiveList, LiveObject } from "@liveblocks/client";

const { room, leave } = client.enterRoom("my-room-id", {
  // +++
  initialStorage: {
    title: "Untitled",
    shapes: new LiveList([
      new LiveObject({ type: "rectangle", color: "yellow" }),
    ]),
  },
  // +++

  // Other options
  // ...
});
```

If a new top-level property is added to `initialStorage`, the next time a user
connects, the new property will be created. Other properties will be unaffected.
Any
[conflict-free live structures](/docs/api-reference/liveblocks-client#Storage)
and JSON-serializable objects are allowed (the `LsonObject` type).

#### Speed up connecting to a room [#speed-up-connecting-to-a-room]

To speed up connecting to a room, you can call
[`Liveblocks.prewarmRoom`](/docs/api-reference/liveblocks-node#get-rooms-roomId-prewarm)
on the server, which will warm up a room for the next 10 seconds. Triggering
this directly before a user navigates to a room is an easy to way use this API.

### Client.getRoom

Gets a room by its ID. Returns `null` if [`client.enterRoom`][] has not been
called previously.

```ts
const room = client.getRoom("my-room");
```

It’s unlikely you’ll need this API if you’re using the newer
[`client.enterRoom`][] API. Note that it’s possible to
[add types to your room](#typing-your-data).

<PropertiesList title="Returns">
  <PropertiesListItem
    name="room"
    type="Room<Presence, Storage, UserMeta, RoomEvent> | null"
  >
    A [Room](#Room), used for building your Liveblocks application. Returns
    `null` if the room has not yet been joined by the current client. Learn more
    about [typing your room](#typing-your-data).
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="roomId" type="string" required>
    The ID of the room you’re connecting to.
  </PropertiesListItem>
</PropertiesList>

### Client.getSyncStatus

Gets the current Liveblocks synchronization status.

```ts
const syncStatus = client.getSyncStatus();
// "synchronizing" | "synchronized"
```

<PropertiesList title="Returns">
  <PropertiesListItem name="returns" type={'"synchronizing" | "synchronized"'}>
    Will be `"synchronizing"` if there are any local changes to any part of
    Liveblocks that still need to be acknowledged by the server. Will be
    `"synchronized"` when all local changes have been persisted.
  </PropertiesListItem>
</PropertiesList>

### Client.logout

Purges any auth tokens from the client’s memory. If there are any rooms that are
still connected, they will be forced to reauthorize.

```ts
client.logout();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

#### When to logout

Use this function if you have a single page application (SPA) and you wish to
log your user out, and reauthenticate them. This is a way to update your user’s
`info` after a connection has begun.

## AI Copilots

### defineAiTool

Create a custom tool for your AI copilot to use. Defining tools allow the AI
copilot to look up information on-demand, render your own components based on
the tool’s arguments, or perform actions in your application on behalf of the
current user, such as creating content, updating the application state, or
interacting with external services.

```tsx
import { defineAiTool } from "@liveblocks/client";

const myTool = defineAiTool()({
  description: "Fetch user information by ID",
  parameters: {
    type: "object",
    properties: {
      userId: { type: "string", description: "The user’s unique identifier" },
    },
    required: ["userId"],
    additionalProperties: false,
  },
  execute: async ({ userId }) => {
    const user = await getUserById(userId);
    return { data: { user } };
  },
  render: ({ result }) => (
    <AiTool title="User Lookup" icon="👤">
      {!result.data ? (
        <div>Looking up user...</div>
      ) : (
        <div>Found user: {result.data.user.name}</div>
      )}
    </AiTool>
  ),
});
```

<Banner>

Note that the function should be called like this `defineAiTool()({ ... })`
(double-parens). This allows TypeScript’s inference to work correctly. For the
best type inference experience, TypeScript 5.3 or higher is recommended. While
Liveblocks supports TypeScript 5.0+, full type inference for `defineAiTool()`
requires TypeScript 5.3+.

</Banner>

<PropertiesList title="Returns">
  <PropertiesListItem name="tool" type="AiOpaqueToolDefinition">
    An AI tool.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="description" type="string" required>
    A clear description of what the tool does. Used by AI to understand when to
    call this tool.
  </PropertiesListItem>
  <PropertiesListItem name="parameters" type="JSONSchema7" required>
    JSON Schema defining the tool’s input parameters. The AI will validate
    arguments against this schema.
  </PropertiesListItem>
  <PropertiesListItem name="enabled" type="boolean">
    Whether this tool should be enabled. When set to `false`, the tool will not
    be made available to the AI copilot for any new/future chat messages, but
    will still allow existing tool invocations to be rendered that are part of
    the historic chat record. Defaults to true.
  </PropertiesListItem>
  <PropertiesListItem name="execute" type="function">
    Async function that performs the tool’s action. Receives validated arguments
    and execution context, returns structured data. See [implementing the tool
    call via `execute`](#implement-via-execute).
  </PropertiesListItem>
  <PropertiesListItem name="render" type="function">
    React component function that renders the tool’s UI during different
    execution stages. See [tool call rendering
    stages](#tool-call-rendering-stages).
  </PropertiesListItem>
</PropertiesList>

Tools can be registered globally with
[`RegisterAiTool`](/docs/api-reference/liveblocks-react#RegisterAiTool) or
passed directly to [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat).

### Tool call rendering stages [#tool-call-rendering-stages]

Rendering a tool call can be done before the tool call is executed, which allows
you to display a UI during its entire lifecycle. The tool call stages are:

- `receiving` (since [3.4]()) The tool call is being received and its args are
  being streamed in. During this stage, you can access `partialArgs` to display
  a UI while the tool call arguments are still being constructed, but before the
  tool call is executed.
- `executing` The tool call is currently executing, or is ready to be. In this
  stage, the `args` are fully known, but the result of the tool call is not
  known yet.
- `executed` The tool call has been executed, and the result is known. This
  happens after your `execute` function was run, or after you called `respond()`
  inside `render`. In this stage, the `result` object will be available.

The render component will automatically re-render when its stage changes.

### Implementing tool calls

When you implement a tool call, use one of these combinations:

1. Implement `execute` _and_ `render`
1. Implement only `execute`, but no `render`
1. Implement only `render`, but make sure to eventually call `respond()`

#### Implementing your tool call via `execute` [#implement-via-execute]

If you implement the `execute` function, this function will automatically be
invoked when the tool call gets made. The return value of this function will be
the result that will be passed back to the AI copilot.

- `{ data: any, description?: string }` The data to return in case of success.
  `data` must be a legal JSON value. Providing a description is optional. If you
  provide a description, it will be passed to the AI copilot to help it
  understand the returned data or to provide follow-up instructions for how to
  respond to this tool result.
- `{ error: string }` The error message in case the tool call failed to execute.
- `{ cancel: true | string }` If the tool call should be cancelled. You can
  optionally provide a cancel reason as an instruction to the AI copilot.

The returned value can be observed in the `render` method, through the `result`
param:

```tsx
defineAiTool()({
  /* ... */

  execute: async () => {
    await sleep(1000);
    return { data: { user: { name: "Alice" } } };
  },

  render: ({ result }) => {
    if (result.data) {
      return <div>Found user: {result.data.user.name}</div>;
    }

    // Tool hasn’t executed yet
    return <Spinner />;
  },
});
```

If you do not implement `render` alongside `execute`, the tool call will still
be executed, but no UI will be displayed. The result will still be passed back
to the AI copilot.

#### Implementing your tool call via `render` [#implement-via-render]

Sometimes you may not want to immediately execute the tool call. This is most
common to build a Human-in-the-Loop (HITL) style UI where you want the user to
confirm or correct the tool call’s behavior. In these scenarios, you do not want
to implement `execute`. Instead, you could display any UI, as long as you
eventually call the `respond` function that is provided to `render`’s props.

```tsx
defineAiTool()({
  /* ... */
  /* NOTE: No execute method used here! */

  render: ({ respond }) => {
    return (
      <div>
        <button
          onClick={() => {
            respond({ data: { user: { name: "Alice" } } });
          }}
        >
          Confirm
        </button>
        <button
          onClick={() => {
            respond({ cancel: true });
          }}
        >
          Cancel
        </button>
      </div>
    );
  },
});
```

In this example, until the Confirm button is clicked, the AI chat will remain in
“executing” stage, awaiting the result of this tool call.

This example is for illustrative purposes only. In practice, using our
[`AiTool.Confirmation`](/docs/api-reference/liveblocks-react-ui#AiTool.Confirmation)
tool is preferred for building confirm/cancel flows.

Like with the `execute` function, the `respond` function should be called with a
value of this shape:

- `{ data: any, description?: string }` The data to return in case of success.
  `data` must be a legal JSON value. Providing a description is optional. If you
  provide a description, it will be passed to the AI copilot to help it
  understand the returned data or to provide follow-up instructions for how to
  respond to this tool result.
- `{ error: string }` The error message in case the tool call failed to execute.
- `{ cancel: true | string }` If the tool call should be cancelled. You can
  optionally provide a cancel reason as an instruction to the AI copilot.

#### Handling different tool call stages [#handling-stages]

You can handle all three stages of a tool call in your render function to
provide a smooth user experience during tool call streaming and execution:

```tsx
const bookFlightTool = defineAiTool()({
  description: "Book a flight for a user",
  parameters: {
    type: "object",
    properties: {
      origin: { type: "string", description: "Departure city" },
      destination: { type: "string", description: "Arrival city" },
      departureDate: {
        type: "string",
        description: "Departure date (YYYY-MM-DD)",
      },
      passengers: {
        type: "array",
        items: {
          type: "object",
          properties: {
            name: { type: "string" },
            age: { type: "number" },
          },
          required: ["name", "age"],
          additionalProperties: false,
        },
        description: "List of passengers",
      },
    },
    required: ["origin", "destination", "departureDate", "passengers"],
    additionalProperties: false,
  },
  execute: async ({ origin, destination, departureDate, passengers }) => {
    const booking = await bookFlight({
      origin,
      destination,
      departureDate,
      passengers,
    });
    return { data: { bookingId: booking.id } };
  },
  render: ({ stage, partialArgs, args, result }) => {
    return (
      <AiTool title="Flight Booking" icon="✈️">
        {stage === "receiving" && (
          <div>
            <h4>Preparing flight booking...</h4>
            {partialArgs.origin && <p>From: {partialArgs.origin}</p>}
            {partialArgs.destination && <p>To: {partialArgs.destination}</p>}
            {partialArgs.departureDate && (
              <p>Date: {partialArgs.departureDate}</p>
            )}
            {partialArgs.passengers && (
              <div>
                <p>Passengers ({partialArgs.passengers.length}):</p>
                <ul>
                  {partialArgs.passengers.map((passenger, index) => (
                    <li key={index}>
                      {passenger?.name || "Loading..."}
                      {passenger?.age && ` (${passenger.age})`}
                    </li>
                  ))}
                </ul>
              </div>
            )}
          </div>
        )}
        {stage === "executing" && (
          <div>
            <h4>Booking flight...</h4>
            <p>
              {args.origin} → {args.destination} on {args.departureDate}
            </p>
            <p>{args.passengers.length} passenger(s)</p>
          </div>
        )}
        {stage === "executed" && result.data && (
          <div>
            <h4>Flight booked successfully!</h4>
            <p>Booking ID: {result.data.bookingId}</p>
          </div>
        )}
      </AiTool>
    );
  },
});
```

In this example, the tool arguments stream in progressively during the
`receiving` stage, causing multiple re-renders as each field appears:

- **1st render**: `{ stage: "receiving", partialArgs: {} }`
- **2nd render**: `{ stage: "receiving", partialArgs: { origin: "New York" } }`
- **3rd render**:
  `{ stage: "receiving", partialArgs: { origin: "New York", destination: "London" } }`
- **4th render**:
  `{ stage: "receiving", partialArgs: { origin: "New York", destination: "London", departureDate: "2024-12-15" } }`
- **5th render**: `{ stage: "receiving", partialArgs: { ..., passengers: [] } }`
- **6th render**:
  `{ stage: "receiving", partialArgs: { ..., passengers: [{ name: "John" }] } }`
- **7th render**:
  `{ stage: "receiving", partialArgs: { ..., passengers: [{ name: "John", age: 3 }] } }`
- **8th render**:
  `{ stage: "receiving", partialArgs: { ..., passengers: [{ name: "John", age: 30 }] } }`
- **Final render**: `{ stage: "executing", args: { /* complete object */ } }`

This demonstrates how each field and nested property appears incrementally,
providing real-time feedback to users as the AI constructs the tool call
arguments.

Arguments are streamed in forward-only order. Once a field begins appearing, all
previous fields are complete and won’t be modified. You’ll never see
`{ origin: "New York", destination: "London" }` followed by
`{ origin: "San Francisco", destination: "London" }`, but you might see
`{ origin: "New" }` then `{ origin: "New York" }` then
`{ origin: "New York", destination: "London" }`.

## Room

Room returned by [`client.enterRoom`][] (or [`client.getRoom`][]).

### Room.getPresence

Return the current user’s Presence.
[Presence](/docs/ready-made-features/presence) is used to store custom
properties on each user that exist until the user disconnects. An example use
would be storing a user’s cursor coordinates.

```ts
const presence = room.getPresence();

// { cursor: { x: 363, y: 723 } }
console.log(presence);
```

Presence is set with [`updatePresence`](#Room.updatePresence) and can be typed
when you [enter a room](#enter-room-typing-a-room). The example above is using
the following type:

```ts file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    Presence: {
      cursor: { x: number; y: number };
    };
  }
}
```

<PropertiesList title="Returns">
  <PropertiesListItem name="presence" type="TPresence">
    An object holding the Presence value for the currently connected user.
    Presence is set with [`updatePresence`](#Room.updatePresence). Will always
    be JSON-serializable. `TPresence` is the `Presence` type you set yourself,
    [learn more](#Typing-presence).
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.updatePresence

Updates the current user’s [Presence](/docs/ready-made-features/presence). Only
pass the properties you wish to update—any changes will be merged into the
current presence. The entire presence object will not be replaced.

```ts
room.updatePresence({ typing: true });
room.updatePresence({ status: "Online" });

// { typing: true, status: "Online" }
const presence = room.getPresence();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="update" type="TPresence" required>
    The updated Presence properties for the current user inside an object. The
    user’s entire Presence object will not be replaced, instead these properties
    will be merged with the existing Presence. This object must be
    JSON-serializable.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.addToHistory"
    type="boolean"
    defaultValue="false"
  >
    Adds Presence values to the history stack, meaning using undo and redo
    functions will change them. [Learn more](#add-presence-to-history).
  </PropertiesListItem>
</PropertiesList>

#### Add Presence to history [#add-presence-to-history]

By default, Presence values are not added to history. However, using the
`addToHistory` option will add items to the undo/redo stack.

```ts
room.updatePresence({ color: "blue" }, { addToHistory: true });
room.updatePresence({ color: "red" }, { addToHistory: true });
room.history.undo();

// { color: "blue" }
const presence = room.getPresence();
```

See [`room.history`][] for more information.

### Room.getOthers

Returns an array of currently connected users in the room. Returns a
[`User`](#user-type) object for each user. Note that you can also subscribe to
others using [`Room.subscribe("others")`](#Room.subscribe.others).

```ts
const others = room.getOthers();

for (const other of others) {
  const { connectionId, id, info, presence, canWrite, canComment } = other;
  // Do things
}
```

<PropertiesList title="Returns">
  <PropertiesListItem name="others" type="User<Presence, UserMeta>[]">
    An array holding each connected user’s [`User`](#user-type) object. `User`
    contains the current user’s Presence value, along with other information.
    Presence is set with [`updatePresence`](#Room.updatePresence). Returns an
    empty array when no other users are currently connected. Will always be
    JSON-serializable.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.broadcastEvent

Broadcast an event to other users in the Room. Events broadcast to the room can
be listened to with [`Room.subscribe("event")`][]. Takes a custom event payload
as first argument. Should be serializable to JSON.

```ts
room.broadcastEvent({ type: "REACTION", emoji: "🔥" });
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="event" type="TRoomEvent" required>
    The event to broadcast to every other user in the room. Must be
    JSON-serializable. `TRoomEvent` is the `RoomEvent` type you set yourself,
    [learn more](#typing-multiple-events).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.shouldQueueEventIfNotReady"
    type="boolean"
    defaultValue="false"
  >
    Queue the event if the connection is currently closed, or has not been
    opened yet. We’re not sure if we want to support this option in the future
    so it might be deprecated to be replaced by something else. [Learn
    more](#broadcasting-an-event-when-disconnected).
  </PropertiesListItem>
</PropertiesList>

#### Receiving an event

To receive an event, use [`Room.subscribe("event")`][]. The `user` property
received on the other end is the sender’s [`User`](#user-type) instance.

```ts
// User 1
room.broadcastEvent({ type: "REACTION", emoji: "🔥" });

// User 2
const unsubscribe = room.subscribe("event", ({ event, user, connectionId }) => {
  //                                                  ^^^^ User 1
  if (event.type === "REACTION") {
    // Do something
  }
});
```

We recommend using a property such as `type`, so that it’s easy to distinguish
between different events on the receiving end.

#### Typing multiple events [#typing-multiple-events]

When [defining your types](#typing-your-data), you can pass a `RoomEvent` type
in your config file to receive type hints in your app. To define multiple
different custom events, use a union.

```ts
declare global {
  interface Liveblocks {
    RoomEvent:
      | { type: "REACTION"; emoji: string }
      | { type: "ACTION"; action: string };
  }
}
```

```ts
room.subscribe("event", ({ event, user, connectionId }) => {
  if (event.type === "REACTION") {
    // Do something
  }
  if (event.type === "ACTION") {
    // Do something else
  }
});
```

#### Broadcasting an event when disconnected [#broadcasting-an-event-when-disconnected]

By default, broadcasting an event is a “fire and forget” action. If the sending
client is not currently connected to a room, the event is simply discarded. When
passing the `shouldQueueEventIfNotReady` option, the client will queue up the
event, and only send it once the connection to the room is (re)established.

<Banner>

We’re not sure if we want to support `shouldQueueEventIfNotReady` in the future,
so it may be deprecated and replaced with something else.

</Banner>

```ts
room.broadcastEvent(
  { type: "REACTION", emoji: "🔥" },
  {
    // +++
    shouldQueueEventIfNotReady: true,
    // +++
  }
);
```

### Room.getSelf

Gets the current [`User`](#user-type). Returns `null` if the client is not yet
connected to the room.

```ts
const { connectionId, presence, id, info, canWrite, canComment } =
  room.getSelf();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="user" type="User<Presence, UserMeta> | null">
    Returns the current [`User`](#user-type). Returns `null` if the client is
    not yet connected to the room.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

Here’s an example of a full return value, assuming `Presence` and `UserMeta`
[have been set](#user-type).

```ts
const user = room.getSelf();

// {
//   connectionId: 52,
//   presence: {
//     cursor: { x: 263, y: 786 },
//   },
//   id: "mislav.abha@example.com",
//   info: {
//     avatar: "/mislav.png",
//   },
//   canWrite: true,
//   canComment: true,
// }
console.log(user);
```

### Room.getStatus

Gets the current WebSocket connection status of the room. The possible value
are: `initial`, `connecting`, `connected`, `reconnecting`, or `disconnected`.

```ts
const status = room.getStatus();

// "connected"
console.log(status);
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="status"
    type={`"initial" | "connecting" | "connected" | "reconnecting" | "disconnected"`}
  >
    <div className="-mb-3">
    Returns the room’s current connection status. It can return one of five values:
    </div>

    - `"initial"` The room has not attempted to connect yet.
    - `"connecting"` The room is currently authenticating or connecting.
    - `"connected"` The room is connected.
    - `"reconnecting"` The room has disconnected, and is trying to connect again.
    - `"disconnected"` The room is disconnected, and is no longer attempting to connect.

  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.getStorageStatus

Get the Storage status. Use this to tell whether Storage has been synchronized
with the Liveblocks servers.

```ts
const status = room.getStorageStatus();

// "synchronizing"
console.log(status);
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="status"
    type={`"not-loaded" | "loading" | "synchronizing" | "synchronized"`}
  >
    <div className="-mb-3">
    The current room’s Storage status. `status` can be one of four types.
    </div>

    - `"not-loaded"` Storage has not been loaded yet as [`room.getStorage`][] has not been called.
    - `"loading"` Storage is currently loading for the first time.
    - `"synchronizing"` Local Storage changes are currently being synchronized.
    - `"synchronized"` Local Storage changes have been synchronized.

  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.subscribe(storageItem)

Subscribe to updates on a particular storage item, and takes a callback function
that’s called when the storage item is updated. The Storage `root` is a
[`LiveObject`][], which means you can subscribe to this, as well as other live
structures. Returns an unsubscribe function.

```ts
const { root } = await room.getStorage();

const unsubscribe = room.subscribe(root, (updatedRoot) => {
  // Do something
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="unsubscribe" type="() => void">
    Unsubscribe function. Call it to cancel the subscription.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="storageItem"
    type="L extends (LiveObject | LiveMap | LiveList)"
    required
  >
    The `LiveObject`, `LiveMap`, or `LiveList` which is being subscribed to.
    Each time the structure is updated, the callback is called.
  </PropertiesListItem>
  <PropertiesListItem name="callback" type="(node: L) => void" required>
    Function that’s called when `storageItem` updates. Returns the updated
    storage structure.
  </PropertiesListItem>
  <PropertiesListItem name="options.isDeep" type="boolean">
    Subscribe to both `storageItem` and its children. The callback function will
    be passed a list of updates instead of just the new Storage item. [Learn
    more](#listening-for-nested-changes).
  </PropertiesListItem>
</PropertiesList>

#### Typing Storage

To type the Storage values you receive, make sure to set your `Storage` type.

```ts file="liveblocks.config.ts"
import { LiveList } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    Storage: {
      animals: LiveList<{ name: string }>;
    };
  }
}
```

The type received in the callback will match the type passed. Learn more under
[typing your room](#typing-your-data).

```ts
const { root } = await room.getStorage();
const animals = root.get("animals");

const unsubscribe = room.subscribe(animals, (updatedAnimals) => {
  // LiveList<[{ name: "Fido" }, { name: "Felix" }]>
  console.log(updatedAnimals);
});
```

#### Subscribe to any live structure

You can subscribe to any live structure, be it the Storage `root`, a child, or a
structure even more deeply nested.

```ts file="liveblocks.config.ts"
import { LiveMap, LiveObject } from "@liveblocks/client";

type Person = LiveObject<{ name: string; age: number }>;

declare global {
  interface Liveblocks {
    Storage: {
      people: LiveMap<string, Person>;
    };
  }
}
```

```ts
const { root } = await room.getStorage();
const people = root.get("people");
const steven = people.get("steven");

const unsubscribeRoot = room.subscribe(root, (updatedRoot) => {
  // ...
});

const unsubscribePeople = room.subscribe(people, (updatedPeople) => {
  // ...
});

const unsubscribeSteven = room.subscribe(steven, (updatedSteven) => {
  // ...
});
```

#### Listening for nested changes [#listening-for-nested-changes]

It’s also possible to subscribe to a Storage item and all of its children by
passing an optional `isDeep` option in the third argument. In this case, the
callback will be passed a list of updates instead of just the new Storage item.
Each such update is a `{ type, node, updates }` object.

```ts
const { root } = await room.getStorage();

const unsubscribe = room.subscribe(
  root,
  (storageUpdates) => {
    for (const update of storageUpdates) {
      const {
        type, // "LiveObject", "LiveList", or "LiveMap"
        node,
        updates,
      } = update;
      switch (type) {
        case "LiveObject": {
          // updates["property"]?.type; is "update" or "delete"
          // update.node is the LiveObject that has been updated/deleted
          break;
        }
        case "LiveMap": {
          // updates["key"]?.type; is "update" or "delete"
          // update.node is the LiveMap that has been updated/deleted
          break;
        }
        case "LiveList": {
          // updates[0]?.type; is "delete", "insert", "move", or "set"
          // update.node is the LiveList that has been updated, deleted, or modified
          break;
        }
      }
    }
  },
  { isDeep: true }
);
```

#### Using async functions

You use an `async` function inside the subscription callback, though bear in
mind that the callback itself is synchronous, and there’s no guarantee the
`async` function will complete before the callback is run again.

```ts
const { root } = await room.getStorage();

const unsubscribe = room.subscribe(root, (updatedRoot) => {
  async function doThing() {
    await fetch(/* ... */);
  }

  doThing();
});
```

If the order of updates is important in your application, and it’s important to
ensure that your `async` function doesn’t start before the previous one
finishes, you can use a package such as
[`async-mutex`](https://www.npmjs.com/package/async-mutex) to help you with
this. Using `runExclusive` will effectively form a queue for all upcoming
updates, guaranteeing serial execution.

```ts
import { Mutex } from "async-mutex";

const { root } = await room.getStorage();
const myMutex = new Mutex();

const unsubscribeUpdates = room.subscribe(root, (root) => {
  void myMutex.runExclusive(async () => {
    await fetch(/* ... */);
  });
});
```

Note that this may cause a performance penalty in your application, as certain
updates will be ignored.

### Room.subscribe("event") [#Room.subscribe.event]

Subscribe to events broadcast by [`Room.broadcastEvent`][]. Takes a callback
that’s run when another user calls [`Room.broadcastEvent`][]. Provides the
`event` along with the `user` and their `connectionId` of the user that sent the
message. Returns an unsubscribe function.

```ts
// User 1
room.broadcastEvent({ type: "REACTION", emoji: "🔥" });

// +++
// User 2
const unsubscribe = room.subscribe("event", ({ event, user, connectionId }) => {
  //                                                  ^^^^ Will be User 1
  if (event.type === "REACTION") {
    // Do something
  }
});
// +++
```

<PropertiesList title="Returns">
  <PropertiesListItem name="unsubscribe" type="() => void">
    Unsubscribe function. Call it to cancel the subscription.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="eventType" type={`"event"`} required>
    Listen to events.
  </PropertiesListItem>
  <PropertiesListItem
    name="callback"
    detailedType="(event: RoomEventMessage<Presence, UserMeta, RoomEvent>) => void"
    required
  >
    Function that’s called when another user sends an event. Receives the event,
    the [`user`](#user-type) that sent the event, and their `connectionId`. If
    this event was sent via
    [`liveblocks.broadcastEvent`](/docs/api-reference/liveblocks-node#post-broadcast-event)
    or the [Broadcast event
    API](/docs/api-reference/rest-api-endpoints#post-broadcast-event), `user`
    will be `null` and `connectionId` will be `-1`. [Learn
    more](#receiving-events-from-the-server)
  </PropertiesListItem>
</PropertiesList>

#### Typing events

When [defining your types](#typing-your-data), you can pass a `RoomEvent` type
to your config file to receive type hints in your app. To define multiple
different custom events, use a union.

```ts
declare global {
  interface Liveblocks {
    RoomEvent:
      | { type: "REACTION"; emoji: string }
      | { type: "ACTION"; action: string };
  }
}
```

```ts
room.subscribe("event", ({ event, user, connectionId }) => {
  if (event.type === "REACTION") {
    // Do something
  }
  if (event.type === "ACTION") {
    // Do something else
  }
});
```

#### Receiving events from the server [#receiving-events-from-the-server]

Events can be received from the server with either
[`liveblocks.broadcastEvent`](/docs/api-reference/liveblocks-node#post-broadcast-event)
or the
[Broadcast Event API](/docs/api-reference/rest-api-endpoints#post-broadcast-event).
In events sent from the server, `user` will be `null`, and `connectionId` will
be `-1`.

```ts
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST() {
  await liveblocks.broadcastEvent({ type: "REACTION", emoji: "🔥" });
}
```

```ts
const unsubscribe = room.subscribe("event", ({ event, user, connectionId }) => {
  // `null`, `-1`
  console.log(user, connectionId);
});
```

### Room.subscribe("my-presence") [#Room.subscribe.my-presence]

Subscribe to the current user’s Presence. Takes a callback that is called every
time the current user presence is updated with [`Room.updatePresence`][].
Returns an unsubscribe function.

```ts
const unsubscribe = room.subscribe("my-presence", (presence) => {
  // Do something
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="unsubscribe" type="() => void">
    Unsubscribe function. Call it to cancel the subscription.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="eventType" type={`"my-presence"`} required>
    Listen to the current user’s presence.
  </PropertiesListItem>
  <PropertiesListItem
    name="callback"
    type="(presence: TPresence) => void"
    required
  >
    Function that’s called when the current user’s Presence has updated, for
    example with [`Room.updatePresence`][]. Receives the updates Presence value.
  </PropertiesListItem>
</PropertiesList>

#### Typing Presence

To type the Presence values you receive, make sure to set your Presence type.

```ts file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    Presence: {
      status: string;
      cursor: { x: number; y: number };
    };
  }
}
```

The type received in the callback will match the type passed. Learn more under
[typing your data](#typing-your-data).

```ts
const unsubscribe = room.subscribe("my-presence", (presence) => {
  // { status: "typing", cursor: { x: 45, y: 67 }
  console.log(presence);
});
```

### Room.subscribe("others") [#Room.subscribe.others]

Subscribe to every other users’ updates. Takes a callback that’s called when a
user’s Presence updates, or when they enter or leave the room. Returns an
unsubscribe function.

```ts
const unsubscribe = room.subscribe("others", (others, event) => {
  // Do something
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="unsubscribe" type="() => void">
    Unsubscribe function. Call it to cancel the subscription.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="eventType" type={`"others"`} required>
    Listen to others.
  </PropertiesListItem>
  <PropertiesListItem
    name="callback"
    type="(others: User<Presence, UserMeta>[], event: OthersEvent) => void"
    required
  >
    Function that’s called when another user’s Presence has updated, for example
    with [`Room.updatePresence`][], or an others event has occurred. Receives an
    array of [`User`](#user-type) values for each currently connected user. Also
    received an object with information about the event that has triggered the
    update, [learn more](#listening-for-others-events).
  </PropertiesListItem>
</PropertiesList>

#### Typing Presence

To type the Presence values you receive, make sure to set your Presence type.

```ts file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    Presence: {
      status: string;
      cursor: { x: number; y: number };
    };
  }
}
```

The type received in the callback will match the type passed. Learn more under
[typing your data](#typing-your-data).

```ts
const unsubscribe = room.subscribe("others", (others, event) => {
  // { status: "typing", cursor: { x: 45, y: 67 }
  console.log(others[0].presence);
});
```

#### Listening for others events [#listening-for-others-events]

The `event` parameter returns information on why the callback has just run, for
example if their Presence has updated, if they’ve just left or entered the room,
or if the current user has disconnected.

```ts
const unsubscribe = room.subscribe("others", (others, event) => {
  if (event.type === "leave") {
    // A user has left the room
    // event.user;
  }

  if (event.type === "enter") {
    // A user has entered the room
    // event.user;
  }

  if (event.type === "update") {
    // A user has updated
    // event.user;
    // event.updates;
  }

  if (event.type === "reset") {
    // A disconnection has occurred and others has reset
  }
});
```

#### Live cursors

Here’s a basic example showing you how to render live cursors.
[`Room.updatePresence`](/docs/api-reference/liveblocks-client#Room.updatePresence)
is being used to update each user’s cursor position.

```ts file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    Presence: {
      cursor: { x: number; y: number };
    };
  }
}
```

```ts
const { room, leave } = client.enterRoom("my-room-id");

// Call this to update the current user’s Presence
function updateCursorPosition({ x, y }) {
  room.updatePresence({ cursor: { x, y } });
}

const others = room.getOthers();

// Run __renderCursor__ when any other connected user updates their presence
const unsubscribe = room.subscribe("others", (others, event) => {
  for (const { id, presence } of others) {
    const { x, y } = presence.cursor;
    __renderCursor__(id, { x, y });
  }
}

// Handle events and rendering
// ...
```

Check our [examples page](/examples/browse/cursors) for live demos.

### Room.subscribe("status") [#Room.subscribe.status]

Subscribe to WebSocket connection status updates. Takes a callback that is
called whenever the connection status changes. Possible value are: `initial`,
`connecting`, `connected`, `reconnecting`, or `disconnected`. Returns an
unsubscribe function.

```ts
const unsubscribe = room.subscribe("status", (status) => {
  // "connected"
  console.log(status);
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="unsubscribe" type="() => void">
    Unsubscribe function. Call it to cancel the subscription.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="eventType" type={`"status"`} required>
    Listen to status updates.
  </PropertiesListItem>
  <PropertiesListItem
    name="callback"
    detailedType={`(status: "initial" | "connecting" | "connected" | "reconnecting" | "disconnected") => void`}
    required
  >
    <div className="-mb-3">
    Function that’s called when the room’s connection status has changed. It can return one of five values:
    </div>

    - `"initial"` The room has not attempted to connect yet.
    - `"connecting"` The room is currently authenticating or connecting.
    - `"connected"` The room is connected.
    - `"reconnecting"` The room has disconnected, and is trying to connect again.
    - `"disconnected"` The room is disconnected, and is no longer attempting to connect.

  </PropertiesListItem>
</PropertiesList>

#### When to use status

Status is a low-level API that exposes the WebSocket’s connectivity status. You
can use this, for example, to update a connection status indicator in your UI.
It would be normal for a client to briefly lose the connection and restore it
with quick `connected` → `reconnecting` → `connected` status jumps.

```ts
let indicator = "⚪";

const unsubscribe = room.subscribe("status", (status) => {
  switch (status) {
    case "connecting":
      indicator = "🟡";
      break;
    case "connected":
      indicator = "🟢";
      break;
    // ...
  }
});
```

If you’d like to let users know that there may be connectivity issues, don’t use
this API, but instead refer to [`Room.subscribe("lost-connection")`][] which was
specially built for this purpose.

Do not use this API to detect when Storage or Presence are initialized or
loaded. "Connected" does not guarantee that Storage or Presence are ready. To
detect when Storage is loaded, rely on awaiting the [`Room.getStorage`][]
promise or using the [`Room.subscribe("storage-status")`][] event.

### Room.subscribe("lost-connection") [#Room.subscribe.lost-connection]

A special-purpose event that will fire when a previously connected Liveblocks
client has lost connection, for example due to a network outage, and was unable
to recover quickly. This event is
[designed to help improve UX for your users](#when-to-use-lost-connection-events),
and will not trigger on short interruptions, those that are less than
[5 seconds by default](#setting-lost-connection-timeout). The event only
triggers if a previously connected client disconnects.

```ts
const unsubscribe = room.subscribe("lost-connection", (event) => {
  // "lost"
  console.log(event);
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="unsubscribe" type="() => void">
    Unsubscribe function. Call it to cancel the subscription.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="eventType" type={`"lost-connection"`} required>
    Listen to lost connection events.
  </PropertiesListItem>
  <PropertiesListItem
    name="callback"
    detailedType={`(event: "lost" | "restored" | "failed") => void`}
    required
  >
    <div className="-mb-3">
    Function that’s called when a room’s lost connection event has been triggered. It can return one of three values:
    </div>

    - `"lost"` A connection has been lost for longer than [`lostConnectionTimeout`][].
    - `"restored"` The connection has been restored again.
    - `"failed"` The room has been unable to reconnect again, and is no longer trying. This may happen if a user’s
    network has recovered, but the room’s authentication values no longer allow them to enter.

  </PropertiesListItem>
</PropertiesList>

#### When to use lost connection events [#when-to-use-lost-connection-events]

Lost connections events allows you to build high-quality UIs by warning your
users that the application is still trying to re-establish the connection, for
example through a toast notification. You may want to take extra care in the
mean time to ensure their changes won’t go unsaved, or to help them understand
why they’re not seeing updates made by others yet.

When this happens, this callback is called with the event `lost`. Then, once the
connection restores, the callback will be called with the value `restored`. If
the connection could definitively not be restored, it will be called with
`failed` (uncommon).

```ts
import { toast } from "my-preferred-toast-library";

const unsubscribe = room.subscribe("lost-connection", (event) => {
  switch (event) {
    case "lost":
      toast.warn("Still trying to reconnect...");
      break;

    case "restored":
      toast.success("Successfully reconnected again!");
      break;

    case "failed":
      toast.error("Could not restore the connection");
      break;
  }
});
```

#### Setting lost connection timeout [#setting-lost-connection-timeout]

The [`lostConnectionTimeout`][] configuration option will determine how quickly
the event triggers after a connection loss occurs. By default, it’s set to
`5000`ms, which is 5 seconds.

```ts
import { createClient } from "@liveblocks/client";

const client = createClient({
  // Throw lost-connection event after 5 seconds offline
  lostConnectionTimeout: 5000,

  // ...
});
```

### Room.subscribe("error") [#Room.subscribe.error]

Subscribe to unrecoverable room connection errors. This event will be emitted
immediately before the client disconnects and won’t try reconnecting again.
Returns an unsubscribe function. If you’d like to retry connecting, call
[`room.reconnect`][].

```ts
const unsubscribe = room.subscribe("error", (error) => {
  switch (error.context.code) {
    case -1:
      // Authentication error
      break;

    case 4001:
      // Could not connect because you don’t have access to this room
      break;

    case 4005:
      // Could not connect because room was full
      break;

    case 4006:
      // The room ID has changed, get the new room ID (use this for redirecting)
      const newRoomId = error.message;
      break;

    default:
      // Unexpected error
      break;
  }
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="unsubscribe" type="() => void">
    Unsubscribe function. Call it to cancel the subscription.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="eventType" type={`"error"`} required>
    Listen to error events.
  </PropertiesListItem>
  <PropertiesListItem
    name="callback"
    detailedType={`(error: LiveblocksError) => void`}
    required
  >
    <div className="-mb-3">
    Function that’s called when an unrecoverable error event has been triggered. `error.code` can return one of these
    values:
    </div>

    - `-1` Authentication error.
    - `4001` Could not connect because you don’t have access to this room.
    - `4005` Could not connect because room was full.
    - `4006` The room ID has changed.

  </PropertiesListItem>
</PropertiesList>

#### When to use error events

You can use this event to trigger a “Not allowed” screen/dialog. It can also be
helpful for implementing a redirect to another page.

```ts
const unsubscribe = room.subscribe("error", (error) => {
  // Could not connect because you don’t have access to this room
  if (error.context.code === 4001)
    return __displayForbiddenEntryDialog__();
  }
});
```

#### When a room ID has changed

When a room ID has been changed with
[`liveblocks.updateRoomId`](/docs/api-reference/liveblocks-node#post-rooms-update-roomId)
or the
[Update Room ID API](/docs/api-reference/rest-api-endpoints#post-rooms-update-roomId),
`error.message` will contain the new room ID.

```ts
const unsubscribe = room.subscribe("error", (error) => {
  // The room ID has changed, get the new room ID
  if (error.context.code === 4006)
    const newRoomId = error.message;
    return __redirect__(`/app/${newRoomId}`)
  }
});
```

### Room.subscribe("history") [#Room.subscribe.history]

Subscribe to the current user’s history changes. Returns an unsubscribe
function.

```ts
const unsubscribe = room.subscribe("history", ({ canUndo, canRedo }) => {
  // Do something
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="unsubscribe" type="() => void">
    Unsubscribe function. Call it to cancel the subscription.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="eventType" type={`"history"`} required>
    Listen to history events.
  </PropertiesListItem>
  <PropertiesListItem
    name="callback"
    detailedType={`({ canUndo: boolean, canRedo: boolean }) => void`}
    required
  >
    Function that’s called when the current user’s history changes. Returns
    booleans that describe whether the user can use
    [undo](/docs/api-reference/liveblocks-client#Room.history.undo) or
    [redo](/docs/api-reference/liveblocks-client#Room.history.redo).
  </PropertiesListItem>
</PropertiesList>

### Room.subscribe("storage-status") [#Room.subscribe.storage-status]

Subscribe to Storage status changes. Use this to tell whether Storage has been
synchronized with the Liveblocks servers. Returns an unsubscribe function.

```ts
const unsubscribe = room.subscribe("storage-status", (status) => {
  switch (status) {
    case "not-loaded":
      // Storage has not been loaded yet
      break;
    case "loading":
      // Storage is currently loading
      break;
    case "synchronizing":
      // Local Storage changes are being synchronized
      break;
    case "synchronized":
      // Local Storage changes have been synchronized
      break;
  }
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="unsubscribe" type="() => void">
    Unsubscribe function. Call it to cancel the subscription.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="eventType" type={`"storage-status"`} required>
    Listen to Storage status events.
  </PropertiesListItem>
  <PropertiesListItem
    name="callback"
    detailedType={`(status: "not-loaded" | "loading" | "synchronizing" | "synchronized") => void`}
    required
  >
    <div className="-mb-3">
    Function that’s called when the current user’s Storage updated status have
    changed. `status` can be one of four types.
    </div>

    - `"not-loaded` - Storage has not been loaded yet as [`getStorage`][] has not been called.
    - `"loading"` - Storage is currently loading for the first time.
    - `"synchronizing"` - Local Storage changes are currently being synchronized.
    - `"synchronized"` - Local Storage changes have been synchronized

  </PropertiesListItem>
</PropertiesList>

### Room.batch

Batches Storage and Presence modifications made during the given function. Each
modification is grouped together, which means that other clients receive the
changes as a single message after the batch function has run. When undoing or
redoing these changes, the entire batch will be undone/redone together instead
of atomically.

```ts
const { root } = await room.getStorage();

room.batch(() => {
  root.set("x", 0);
  room.updatePresence({ cursor: { x: 100, y: 100 } });
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="return" type="T">
    Returns the return value from the callback.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="callback" type="() => T" required>
    A callback containing every Storage and Presence notification that will be
    part of the batch. Cannot be an `async` function.
  </PropertiesListItem>
</PropertiesList>

#### When to batch updates

For the most part, _you don’t need to batch updates_. For example, given a
[whiteboard application](/examples/browse/whiteboard), it’s perfectly fine to
update a note’s position on the board multiple times per second, in separate
updates. However, should you implement a “Delete all” button, that may delete 50
notes at once, this is where you should use a batch.

```ts
const { root } = await room.getStorage();
const notes = root.get("notes");

// ✅ Batch simultaneous changes together
room.batch(() => {
  for (const noteId of notes.keys()) {
    notes.delete(noteId);
  }
});
```

This batch places each
[`LiveMap.delete`](/docs/api-reference/liveblocks-client#LiveMap.delete) call
into a single WebSocket update, instead of sending multiple updates. This will
be much quicker.

#### Batching groups history changes

Batching changes will also group changes into a single history state.

```ts
const { root } = await room.getStorage();
const pet = root.set("pet", new LiveObject({ name: "Fido", age: 5 }));

// ✅ Batch groups changes into one
room.batch(() => {
  pet.set("name", "Felix");
  pet.set("age", 10);
});

// { name: "Felix", age: 10 }
pet.toImmutable();

room.history.undo();

// { name: "Fido", age: 5 }
pet.toImmutable();
```

#### Doesn’t work with async functions

Note that `room.batch` cannot take an `async` function.

```tsx
// ❌ Won’t work
room.batch(async () => {
  // ...
});

// ✅ Will work
room.batch(() => {
  // ...
});
```

### Room.history

Room’s history contains functions that let you undo and redo operations made to
Storage and Presence on the current client. Each user has a separate history
stored in memory, and history is reset when the page is reloaded.

```ts
const { undo, redo, pause, resume /*, ... */ } = room.history;
```

<Banner title="History in Yjs">

Note that to undo or redo in Yjs, you must use a separate history manager,
[`Y.UndoManager`](https://docs.yjs.dev/api/undo-manager).

</Banner>

#### Add Presence to history

By default, history is only enabled for Storage. However, you can use the
`addToHistory` option to additionally
[add Presence state to history](/docs/api-reference/liveblocks-client#add-presence-to-history).

```tsx
room.updatePresence({ color: "blue" }, { addToHistory: true });
```

### Room.history.undo

Reverts the last operation. It does not impact operations made by other clients,
and will only undo changes made by the current client.

```ts
const person = new LiveObject();
person.set("name", "Pierre");
person.set("name", "Jonathan");

room.history.undo();

// "Pierre"
root.get("name");
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.history.redo

Restores the last undone operation. It does not impact operations made by other
clients, and will only restore changes made by the current client.

```ts
const person = new LiveObject();
person.set("name", "Pierre");
person.set("name", "Jonathan");

room.history.undo();
room.history.redo();

// "Jonathan"
root.get("name");
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.history.canUndo

Returns true or false, depending on whether there are any operations to undo.
Helpful for disabling undo buttons.

```ts
const person = new LiveObject();
person.set("name", "Pierre");

// true
room.history.canUndo();

room.history.undo();

// false
room.history.canUndo();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="canUndo" type="boolean">
    Whether there is an undo operation in the current history stack.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.history.canRedo

Returns true or false, depending on whether there are any operations to redo.
Helpful for disabling redo buttons.

```ts
const person = new LiveObject();
person.set("name", "Pierre");

// false
room.history.canRedo();

room.history.undo();

// true
room.history.canRedo();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="canRedo" type="boolean">
    Whether there is a redo operation in the current history stack.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.history.clear

Clears the undo and redo stacks for the current client. Explicitly clearing
history resets the ability to undo beyond the current document state. Other
clients’ histories are unaffected.

```ts
const person = new LiveObject();
person.set("name", "Pierre");

// true
room.history.canUndo();

room.history.clear();

// false
room.history.canUndo();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.history.pause

All future modifications made on the Room will be merged together to create a
single history item until resume is called.

```ts
const info = new LiveObject({ time: "one" });

room.history.pause();
info.set("time", "two");
info.set("time", "three");
room.history.resume();

room.history.undo();

// "one"
room.get("time");
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.history.resume

Resumes history after a [pause](#Room.history.pause). Modifications made on the
Room are not merged into a single history item any more.

```ts
const info = new LiveObject({ time: "one" });

room.history.pause();
info.set("time", "two");
info.set("time", "three");
room.history.resume();

room.history.undo();

// "one"
room.get("time");
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.connect

Connect the local room instance to the Liveblocks server. Does nothing if the
room is already connecting, reconnecting or connected. We don’t recommend using
this API directly.

```ts
room.connect();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.reconnect

Reconnect the local room instance to the Liveblocks server, using a new
WebSocket connection.

```ts
room.reconnect();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.disconnect

Disconnect the local room instance from the Liveblocks server. The room instance
will remain functional (for example, it will still allow local presence or
storage mutations), but since it’s no longer connected, changes will not be
persisted or synchronized until the room instance is reconnected again. We don’t
recommend using this API directly.

```ts
room.disconnect();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

## Comments

### Room.getThreads

Returns threads, and their associated inbox notifications and subscriptions,
that are in the current room. It also returns the request date that can be used
for subsequent polling. It’s possible to filter for
[a thread’s resolved status](#filtering-resolved-status) and using
[custom metadata](#filtering-metadata).

```ts
const { threads, inboxNotifications, requestedAt } = await room.getThreads();

// [{ id: "th_s436g8...", type: "thread" }, ...]
console.log(threads);

// [{ id: "in_fwh3d4...", kind: "thread", }, ...]
console.log(inboxNotifications);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="threads" type="ThreadData[]">
    Threads within the current room.
  </PropertiesListItem>
  <PropertiesListItem name="inboxNotifications" type="InboxNotificationData[]">
    Inbox notifications associated with the threads.
  </PropertiesListItem>
  <PropertiesListItem name="subscriptions" type="SubscriptionData[]">
    Subscriptions associated with the threads.
  </PropertiesListItem>
  <PropertiesListItem name="requestedAt" type="Date">
    The request date to use for subsequent polling.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="resolved" type="boolean">
    Only return `resolved` or `unresolved` threads. [Learn more](#filtering-resolved-status).
  </PropertiesListItem>
  <PropertiesListItem name="subscribed" type="boolean">
    Only return `subscribed` or `unsubscribed` threads. [Learn more](#filtering-subscribed-status).
  </PropertiesListItem>
  <PropertiesListItem name="metadata" type="Partial<ThreadMetadata>">
    Only return threads containing the custom metadata. Metadata is set yourself when creating a thread, for example `{ priority: "HIGH" }`. [Learn more](#filtering-metadata).
  </PropertiesListItem>
</PropertiesList>

#### Filtering resolved status [#filtering-resolved-status]

You can filter threads by those that are resolved, or unresolved, by passing a
`boolean` to `query.resolved`.

```ts
// Filtering for threads that are unresolved
const threads = await room.getThreads({
  query: {
    // +++
    resolved: false,
    // +++
  },
});
```

#### Filtering subscribed status [#filtering-subscribed-status]

You can filter threads by those that the user is subscribed to, or not, by
passing a `boolean` to `query.subscribed`.

```ts
// Filtering for threads that the user is subscribed to
const threads = await room.getThreads({
  query: {
    // +++
    subscribed: true,
    // +++
  },
});
```

#### Filtering metadata [#filtering-metadata]

You can define custom metadata when
[creating a thread](/docs/api-reference/liveblocks-client#Room.createThread),
and the `query.metadata` option allows you to return only threads that match.

```ts
// Creating a thread with `priority` metadata
await room.createThread({
  body: {
    // ...
  },
  // +++
  metadata: { priority: "HIGH" },
  // +++
});

// Filtering for threads with the same metadata
const threads = await room.getThreads({
  query: {
    // +++
    metadata: { priority: "HIGH" },
    // +++
  },
});
```

You can also filter for metadata that begins with a specific string.

```ts
// Creating a thread with `{ assigned: "sales:stacy" } metadata
await room.createThread({
  body: {
    // ...
  },
  // +++
  metadata: { assigned: "sales:stacy" },
  // +++
});

// Filtering for threads with `assigned` metadata that starts with `sales:`
const threads = await room.getThreads({
  query: {
    // +++
    metadata: {
      assigned: {
        startsWith: "sales:",
      },
    },
    // +++
  },
});
```

You can also filter for metadata using numeric operators.

```ts
// Creating a thread with `{ posX: 87, level: 5 } metadata
await room.createThread({
  body: {
    // ...
  },
  // +++
  metadata: { posX: 87, level: 5 },
  // +++
});

// Filtering for threads with `posX` greater than 50 and lower than 100, and level greater than or equal to 5
const threads = await room.getThreads({
  query: {
    // +++
    metadata: {
      posX: {
        gt: 50,
        lt: 100,
      },
      level: {
        gte: 5,
      },
    },
    // +++
  },
});
```

### Room.getThreadsSince

Returns threads, and their associated inbox notifications and subscriptions,
that have been updated or deleted since the requested date. Helpful when used in
combination with [`Room.getThreads`](#Room.getThreads) to initially fetch all
threads, then receive updates later.

```ts
const initial = await room.getThreads();

const { threads, inboxNotifications, subscriptions, requestedAt } =
  await room.getThreadsSince({ since: initial.requestedAt });

// { updated: [{ id: "th_s4368s...", type: "thread" }, ...], deleted: [...] }
console.log(threads);

// { updated: [{ id: "in_ds83hs...", kind: "thread", }, ...], deleted: [...] }
console.log(inboxNotifications);

// { updated: [{ subjectId: "th_s4368s...", kind: "thread", }, ...], deleted: [...] }
console.log(subscriptions);
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="threads"
    detailedType="{ updated: ThreadData[], deleted: ThreadDeleteInfo[] }"
  >
    Threads that have been updated or deleted since the requested date.
  </PropertiesListItem>
  <PropertiesListItem
    name="inboxNotifications"
    detailedType="{ updated: InboxNotificationData[], deleted: InboxNotificationDeleteInfo[] }"
  >
    Inbox notifications that have been updated or deleted since the requested
    date.
  </PropertiesListItem>
  <PropertiesListItem
    name="subscriptions"
    detailedType="{ updated: SubscriptionData[], deleted: SubscriptionDeleteInfo[] }"
  >
    Subscriptions that have been updated or deleted since the requested date.
  </PropertiesListItem>
  <PropertiesListItem name="requestedAt" type="Date">
    The request date to use for subsequent polling.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="since" type="Date" required>
    Only return threads that have been updated or deleted after this date.
  </PropertiesListItem>
</PropertiesList>

### Room.getThread

Returns a thread and its associated inbox notification and subscription, from
its ID, if it exists.

```ts
const { thread, inboxNotification, subscription } =
  await room.getThread("th_xxx");
```

The thread ID can be retrieved from existing threads.

```ts
const newThread = await room.createThread(/* ... */);

const { thread, inboxNotification } = await room.getThread(newThread.id);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="thread" type="ThreadData | undefined">
    The requested thread, or `undefined` if it doesn’t exist.
  </PropertiesListItem>
  <PropertiesListItem
    name="inboxNotification"
    type="InboxNotificationThreadData | undefined"
  >
    The inbox notification associated with the thread, or `undefined` if it
    doesn’t exist.
  </PropertiesListItem>
  <PropertiesListItem name="subscription" type="SubscriptionData | undefined">
    The subscription associated with the thread, or `undefined` if it doesn’t
    exist.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="value" type="string" required>
    The ID of the thread you want to retrieve.
  </PropertiesListItem>
</PropertiesList>

### Room.createThread

Creates a thread, and its initial comment, in the current room. A comment’s body
is an array of paragraphs, each containing child nodes, learn more under
[creating thread content](#creating-thread-content).

```ts
const thread = await room.createThread({
  body: {
    version: 1,
    content: [{ type: "paragraph", children: [{ text: "Hello" }] }],
  },
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value" type="ThreadData">
    The thread that has been created.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="body" type="CommentBody" required>
    The content of the comment, see [creating thread
    content](#creating-thread-content).
  </PropertiesListItem>
  <PropertiesListItem name="attachmentIds" type="string[]">
    The IDs of the comment’s attachments.
  </PropertiesListItem>
  <PropertiesListItem name="commentMetadata" type="CommentMetadata">
    Custom metadata to be attached to the initial comment, see [defining comment
    metadata](#defining-comment-metadata).
  </PropertiesListItem>
  <PropertiesListItem name="metadata" type="ThreadMetadata">
    Custom metadata to be attached to the thread, see [defining thread
    metadata](#defining-thread-metadata).
  </PropertiesListItem>
</PropertiesList>

#### Creating thread content [#creating-thread-content]

A comment’s body is an array of paragraphs, each containing child nodes. Here’s
an example of how to construct the following simple comment body, which can be
passed to `room.createThread`.

> Hello **world**
>
> <div className="h-2"></div>
> _Second_ paragraph!

```tsx
import { CommentBody } from "@liveblocks/client";

const body: CommentBody = {
  version: 1,
  content: [
    // +++
    {
      type: "paragraph",
      children: [{ text: "Hello " }, { text: "world", bold: true }],
    },
    {
      type: "paragraph",
      children: [{ text: "Second", italic: true }, { text: " paragraph!" }],
    },
    // +++
  ],
};

const thread = await room.createThread({ body });
```

It’s also possible to create links and mentions.

> <span title="jody.hekla">**@Jody Hekla**</span> the
> **[Liveblocks](https://liveblocks.io)** website is cool!

```ts
const body: CommentBody = {
  version: 1,
  content: [
    // +++
    {
      type: "paragraph",
      children: [
        { type: "mention", id: "jody.hekla" },
        { text: " the " },
        { text: "Liveblocks", type: "link", url: "https://liveblocks.io" },
        { text: " website is cool!" },
      ],
    },
    // +++
  ],
};
```

#### Defining thread metadata [#defining-thread-metadata]

Custom metadata can be attached to each thread. `string`, `number`, and
`boolean` properties are allowed.

```ts
const metadata: Liveblocks["ThreadMetadata"] = {
  color: "blue",
  page: 3,
  pinned: true,
};

const thread = await room.createThread({ body, metadata });
```

### Room.deleteThread

Deletes a thread by its ID.

```ts
await room.deleteThread("th_xxx");
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread to delete.
  </PropertiesListItem>
</PropertiesList>

### Room.editThreadMetadata

Edits a thread’s custom metadata. Metadata can be a `string`, `number`, or
`boolean`. To delete an existing metadata property, set its value to `null`.

```ts
await room.editThreadMetadata({
  threadId: "th_xxx",
  metadata: {
    color: "blue",
    page: 3,
    pinned: true,
  },
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="metadata" type="ThreadMetadata">
    The thread metadata.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread.
  </PropertiesListItem>
  <PropertiesListItem name="metadata" type="Patchable<ThreadMetadata>" required>
    An object containing the metadata properties to update. Metadata can be a
    `string`, `number`, or `boolean`. To delete an existing metadata property,
    set its value to `null`.
  </PropertiesListItem>
</PropertiesList>

### Room.markThreadAsResolved

Marks a thread as resolved.

```ts
await room.markThreadAsResolved("th_xxx");
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread to resolve.
  </PropertiesListItem>
</PropertiesList>

### Room.markThreadAsUnresolved

Marks a thread as unresolved.

```ts
await room.markThreadAsUnresolved("th_xxx");
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread to resolve.
  </PropertiesListItem>
</PropertiesList>

### Room.subscribeToThread

Subscribes the user to a thread, meaning they will receive inbox notifications
when new comments are posted.

```ts
await room.subscribeToThread("th_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value" type="SubscriptionData">
    The thread’s subscription.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread to subscribe to.
  </PropertiesListItem>
</PropertiesList>

#### Replacing room-level subscriptions

Subscribing will replace any existing subscription for the current thread
[set at room-level](#Room.updateSubscriptionSettings). This value can also be
overridden by a room-level call that is run afterwards.

```ts
// 1. Disables notifications for all threads
await room.updateSubscriptionSettings({
  threads: "none",
});

// 2. Enables notifications just for this thread, "th_d75sF3..."
await room.subscribeToThread("th_d75sF3...");

// 3. Disables notifications for all threads, including "th_d75sF3..."
await room.updateSubscriptionSettings({
  threads: "none",
});
```

### Room.unsubscribeFromThread

Unsubscribes the user from a thread, meaning they will no longer receive inbox
notifications when new comments are posted.

```ts
await room.unsubscribeFromThread("th_xxx");
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread to unsubscribe from.
  </PropertiesListItem>
</PropertiesList>

#### Replacing room-level unsubscriptions

Unsubscribing will replace any existing unsubscription for the current thread
[set at room-level](#Room.updateSubscriptionSettings). This value can also be
overridden by a room-level call that is run afterwards.

```ts
// 1. Enable notifications for all threads
await room.updateSubscriptionSettings({
  threads: "all",
});

// 2. Disables notifications just for this thread, "th_d75sF3..."
await room.unsubscribeFromThread("th_d75sF3...");

// 3. Enables notifications for all threads, including "th_d75sF3..."
await room.updateSubscriptionSettings({
  threads: "all",
});
```

### Room.createComment

Creates a comment in a given thread.

```ts
const comment = await room.createComment({
  threadId: "th_xxx",
  body: {
    version: 1,
    content: [{ type: "paragraph", children: [{ text: "Hello" }] }],
  },
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value" type="CommentData">
    The comment that has been created.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread that the comment will be added to.
  </PropertiesListItem>
  <PropertiesListItem name="body" type="CommentBody" required>
    The content of the comment, see [creating comment
    content](#creating-comment-content).
  </PropertiesListItem>
  <PropertiesListItem name="attachmentIds" type="string[]">
    The IDs of the comment’s attachments.
  </PropertiesListItem>
  <PropertiesListItem name="metadata" type="CommentMetadata">
    Custom metadata to be attached to the comment, see [defining comment
    metadata](#defining-comment-metadata).
  </PropertiesListItem>
</PropertiesList>

#### Creating comment content [#creating-comment-content]

A comment’s body is an array of paragraphs, each containing child nodes. Here’s
an example of how to construct the following simple comment body, which can be
passed to `room.createComment`.

> Hello **world**
>
> <div className="h-2"></div>
> _Second_ paragraph!

```tsx
import { CommentBody } from "@liveblocks/client";

const thread = await room.createThread(/* ... */);

const body: CommentBody = {
  version: 1,
  content: [
    // +++
    {
      type: "paragraph",
      children: [{ text: "Hello " }, { text: "world", bold: true }],
    },
    {
      type: "paragraph",
      children: [{ text: "Second", italic: true }, { text: " paragraph!" }],
    },
    // +++
  ],
};

const comment = await room.createComment({ threadId: thread.id, body });
```

It’s also possible to create links and mentions.

> <span title="jody.hekla">**@Jody Hekla**</span> the
> **[Liveblocks](https://liveblocks.io)** website is cool!

```ts
const body: CommentBody = {
  version: 1,
  content: [
    // +++
    {
      type: "paragraph",
      children: [
        { type: "mention", id: "jody.hekla" },
        { text: " the " },
        { text: "Liveblocks", type: "link", url: "https://liveblocks.io" },
        { text: " website is cool!" },
      ],
    },
    // +++
  ],
};
```

#### Defining comment metadata [#defining-comment-metadata]

Custom metadata can be attached to each comment. `string`, `number`, and
`boolean` properties are allowed.

```ts
const metadata: Liveblocks["CommentMetadata"] = {
  priority: 2,
  reviewed: true,
};

const comment = await room.createComment({
  threadId: "th_xxx",
  body,
  metadata,
});
```

### Room.editComment

Edits a comment, replacing its existing comment body and optionally updating its
attachments and metadata. Learn more about
[creating comment content](#creating-comment-content).

```ts
const comment = await room.editComment({
  threadId: "th_xxx",
  commentId: "cm_xxx",
  body: {
    version: 1,
    content: [{ type: "paragraph", children: [{ text: "Hello" }] }],
  },
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value" type="CommentData">
    The comment that has been edited.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread containing the comment.
  </PropertiesListItem>
  <PropertiesListItem name="commentId" type="string" required>
    The ID of the comment that’s being edited.
  </PropertiesListItem>
  <PropertiesListItem name="body" type="CommentBody" required>
    The content of the comment, see [creating comment
    content](#creating-comment-content).
  </PropertiesListItem>
  <PropertiesListItem name="attachmentIds" type="string[]">
    The IDs of the comment’s attachments.
  </PropertiesListItem>
  <PropertiesListItem name="metadata" type="CommentMetadata">
    Custom metadata to be attached to the comment.
  </PropertiesListItem>
</PropertiesList>

### Room.editCommentMetadata

Edits a comment’s custom metadata. Metadata can be a `string`, `number`, or
`boolean`. To delete an existing metadata property, set its value to `null`.

```ts
await room.editCommentMetadata({
  threadId: "th_xxx",
  commentId: "cm_xxx",
  metadata: {
    tag: "important",
    priority: 2,
    flagged: true,
  },
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="metadata" type="CommentMetadata">
    The comment metadata.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread containing the comment.
  </PropertiesListItem>
  <PropertiesListItem name="commentId" type="string" required>
    The ID of the comment.
  </PropertiesListItem>
  <PropertiesListItem
    name="metadata"
    type="Patchable<CommentMetadata>"
    required
  >
    An object containing the metadata properties to update. Metadata can be a
    `string`, `number`, or `boolean`. To delete an existing metadata property,
    set its value to `null`.
  </PropertiesListItem>
</PropertiesList>

### Room.deleteComment

Deletes a comment. If it is the last non-deleted comment, the thread also gets
deleted.

```ts
await room.deleteComment({
  threadId: "th_xxx",
  commentId: "cm_xxx",
});
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Options">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread containing the comment.
  </PropertiesListItem>
  <PropertiesListItem name="commentId" type="string" required>
    The ID of the comment that’s being edited.
  </PropertiesListItem>
</PropertiesList>

### Room.addReaction

Adds a reaction from the current user on a comment.

```ts
const reaction = await room.addReaction({
  threadId: "th_xxx",
  commentId: "cm_xxx",
  emoji: "👍",
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value" type="CommentUserReaction">
    The reaction that has been created.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread containing the comment.
  </PropertiesListItem>
  <PropertiesListItem name="commentId" type="string" required>
    The ID of the comment to add a reaction to.
  </PropertiesListItem>
  <PropertiesListItem name="emoji" type="string" required>
    The emoji reaction to add.
  </PropertiesListItem>
</PropertiesList>

### Room.removeReaction

Removes a reaction from a comment.

```ts
await room.removeReaction({
  threadId: "th_xxx",
  commentId: "cm_xxx",
  emoji: "👍",
});
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Options">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread containing the comment.
  </PropertiesListItem>
  <PropertiesListItem name="commentId" type="string" required>
    The ID of the comment to remove a reaction from.
  </PropertiesListItem>
  <PropertiesListItem name="emoji" type="string" required>
    The emoji reaction to remove.
  </PropertiesListItem>
</PropertiesList>

### Room.prepareAttachment

Creates a local attachment from a file.

```ts
const attachment = room.prepareAttachment({
  file: new File(["Hello, world!"], "hello.txt"),
});

// { "id": "at_1e6nNX...", "name": "hello.txt", "type": "attachment", ... }
console.log(attachment);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value" type="CommentLocalAttachment">
    The local attachment that has been created.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="file" type="File" required>
    The file to create the attachment from.
  </PropertiesListItem>
</PropertiesList>

### Room.uploadAttachment

Uploads a local attachment.

```ts
const attachment = room.prepareAttachment(file);
await room.uploadAttachment(attachment);
```

Optionally, an
[`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
can be passed to cancel the upload.

```ts
const attachment = room.prepareAttachment(file);

// Cancel the upload after 5 seconds
room.uploadAttachment(attachment, { signal: AbortSignal.timeout(5000) });
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value" type="CommentAttachment">
    The attachment that has been uploaded.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="attachment" type="CommentLocalAttachment" required>
    The file to create the attachment from.
  </PropertiesListItem>
  <PropertiesListItem name="options" type="UploadAttachmentOptions">
    A set of options.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="signal" type="AbortSignal">
    Only the inbox notifications updated or deleted after this date will be
    returned.
  </PropertiesListItem>
</PropertiesList>

### Room.getAttachmentUrl

Returns a presigned URL for an attachment by its ID.

```ts
const url = await room.getAttachmentUrl("at_xxx");

// "https://..."
console.log(url);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value" type="string">
    A presigned URL for the attachment.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="attachmentId" type="string" required>
    The ID of the attachment to get the URL for.
  </PropertiesListItem>
</PropertiesList>

## Notifications

### Client.getInboxNotifications

Returns the current user’s inbox notifications and their associated threads and
subscriptions. It also returns the request date that can be used for subsequent
polling.

```ts
const { inboxNotifications, threads, subscriptions, requestedAt } =
  await client.getInboxNotifications();

// [{ id: "in_fwh3d4...", kind: "thread", }, ...]
console.log(inboxNotifications);

// [{ id: "th_s436g8...", type: "thread" }, ...]
console.log(threads);

// [{ subjectId: "th_s436g8...", kind: "thread", }, ...]
console.log(subscriptions);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="inboxNotifications" type="InboxNotificationData[]">
    Current user’s inbox notifications.
  </PropertiesListItem>
  <PropertiesListItem name="threads" type="ThreadData[]">
    Threads associated with the inbox notifications.
  </PropertiesListItem>
  <PropertiesListItem name="subscriptions" type="SubscriptionData[]">
    Subscriptions associated with the inbox notifications.
  </PropertiesListItem>
  <PropertiesListItem name="requestedAt" type="Date">
    The request date to use for subsequent polling.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="roomId" type="string">
    Only return inbox notifications for the given room. [Learn
    more](#filtering-inbox-notifications).
  </PropertiesListItem>
  <PropertiesListItem name="kind" type="string">
    Only return inbox notifications for the kind. [Learn
    more](#filtering-inbox-notifications).
  </PropertiesListItem>
</PropertiesList>

#### Filtering inbox notifications [#filtering-inbox-notifications]

You can filter inbox notifications by those that are associated with a specific
room or kind, by passing a `string` to `query.roomId` or `query.kind`.

```ts
// Filtering for inbox notifications that are associated with a specific room or kind
const { inboxNotifications } = await client.getInboxNotifications({
  query: {
    // +++
    roomId: "room1",
    kind: "thread",
    // +++
  },
});
```

### Client.getInboxNotificationsSince

Returns the updated and deleted inbox notifications and their associated threads
and subscriptions since the requested date. Helpful when used in combination
with [`Client.getInboxNotifications`](#Client.getInboxNotifications) to
initially fetch all notifications, then receive updates later.

```ts
const initial = await client.getInboxNotifications();

const { inboxNotifications, threads, subscriptions, requestedAt } =
  await client.getInboxNotificationsSince({ since: initial.requestedAt });

// { updated: [{ id: "in_ds83hs...", kind: "thread", }, ...], deleted: [...] }
console.log(inboxNotifications);

// { updated: [{ id: "th_s4368s...", type: "thread" }, ...], deleted: [...] }
console.log(threads);

// { updated: [{ subjectId: "th_s4368s...", kind: "thread", }, ...], deleted: [...] }
console.log(subscriptions);
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="inboxNotifications"
    detailedType="{ updated: InboxNotificationData[], deleted: InboxNotificationDeleteInfo[] }"
  >
    Inbox notifications that have been updated or deleted since the requested
    date.
  </PropertiesListItem>
  <PropertiesListItem
    name="threads"
    detailedType="{ updated: ThreadData[], deleted: ThreadDeleteInfo[] }"
  >
    Threads that have been updated or deleted since the requested date.
  </PropertiesListItem>
  <PropertiesListItem name="requestedAt" type="Date">
    The request date to use for subsequent polling.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="since" type="Date" required>
    Only the inbox notifications updated or deleted after this date will be
    returned.
  </PropertiesListItem>
</PropertiesList>

### Client.getUnreadInboxNotificationsCount

Gets the number of unread inbox notifications for the current user.

```ts
const count = await client.getUnreadInboxNotificationsCount();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value" type="number">
    Number of unread inbox notifications.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Client.markAllInboxNotificationsAsRead

Marks all inbox notifications as read, for the current user.

```ts
await client.markAllInboxNotificationsAsRead();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Client.markInboxNotificationAsRead

Marks an inbox notification as read, for the current user.

```ts
await client.markInboxNotificationAsRead("in_xxx");
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="inboxNotificationId" type="string" required>
    The ID of the inbox notification to be marked as read.
  </PropertiesListItem>
</PropertiesList>

### Client.deleteAllInboxNotifications

Deletes an inbox notification for the current user.

```ts
await client.deleteAllInboxNotifications();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Client.deleteInboxNotification

Deletes an inbox notification for the current user.

```ts
await client.deleteInboxNotification("in_xxx");
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="inboxNotificationId" type="string" required>
    The ID of the inbox notification to be deleted.
  </PropertiesListItem>
</PropertiesList>

### Room.getSubscriptionSettings

Gets the user’s subscription settings for the current room. This notates which
[`inboxNotifications`](/docs/api-reference/liveblocks-client#Client.getInboxNotifications)
the current user receives in the current room.

```ts
const settings = await room.getSubscriptionSettings();
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="settings"
    type="{ threads, textMentions }"
  >
    Subscription settings for Liveblocks products.
  </PropertiesListItem>
  <PropertiesListItem
    name="settings.threads"
    type={`"all" | "replies_and_mentions" | "none"`}
  >
    <div className="-mb-3">
      Returns the current room’s subscription settings for threads. It can return one of three values:
    </div>

    - `"all"` Receive notifications for every activity in every thread.
    - `"replies_and_mentions"` Receive notifications for mentions and threads you’re participating in.
    - `"none"` No notifications are received.

  </PropertiesListItem>
    <PropertiesListItem
    name="settings.textMentions"
    type={`"mine" | "none"`}
  >
    <div className="-mb-3">
      Returns the current room’s subscription settings for text mentions. It can be one of two values:
    </div>

    - `"mine"` Receive notifications for mentions of you.
    - `"none"` No notifications are received.

  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Room.updateSubscriptionSettings

Updates the user’s subscription settings for the current room. Updating this
setting will change which
[`inboxNotifications`](/docs/api-reference/liveblocks-client#Client.getInboxNotifications)
the current user receives in the current room.

```ts
const settings = await room.updateSubscriptionSettings({
  threads: "replies_and_mentions",
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="settings"
    type="{ threads, textMentions }"
  >
    Subscription settings for Liveblocks products.
  </PropertiesListItem>
  <PropertiesListItem
    name="settings.threads"
    type={`"all" | "replies_and_mentions" | "none"`}
  >
    <div className="-mb-3">
      Returns the current room’s subscription settings for threads. It can return one of three values:
    </div>

    - `"all"` Receive notifications for every activity in every thread.
    - `"replies_and_mentions"` Receive notifications for mentions and threads you’re participating in.
    - `"none"` No notifications are received.

  </PropertiesListItem>
    <PropertiesListItem
    name="settings.textMentions"
    type={`"mine" | "none"`}
  >
    <div className="-mb-3">
      Returns the current room’s subscription settings for text mentions. It can be one of two values:
    </div>

    - `"mine"` Receive notifications for mentions of you.
    - `"none"` No notifications are received.

  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
    <PropertiesListItem
    name="threads"
    type={`"all" | "replies_and_mentions" | "none"`}
  >
    <div className="-mb-3">
      Sets the current room’s subscription settings for threads. It can be one of three values:
    </div>

    - `"all"` Receive notifications for every activity in every thread.
    - `"replies_and_mentions"` Receive notifications for mentions and threads you’re participating in.
    - `"none"` No notifications are received.

  </PropertiesListItem>
  <PropertiesListItem
    name="textMentions"
    type={`"mine" | "none"`}
  >
    <div className="-mb-3">
      Sets the current room’s subscription settings for text mentions. It can be one of two values:
    </div>

    - `"mine"` Receive notifications for mentions of you.
    - `"none"` No notifications are received.

  </PropertiesListItem>
</PropertiesList>

#### Replacing individual thread subscriptions

Subscribing will replace any
[existing thread subscriptions](#Room.subscribeToThread) in the current room.
This value can also be overridden by a room-level call that is run afterwards.

```ts
// 1. Enables notifications just for this thread, "th_d75sF3..."
await room.subscribeToThread("th_d75sF3...");

// 2. Disables notifications for all threads, including "th_d75sF3..."
await room.updateSubscriptionSettings({
  threads: "none",
});
```

### Client.getNotificationSettings [@badge=Beta]

Returns the user’s notification settings in the current project, in other words
which [notification webhook events](/docs/platform/webhooks#NotificationEvent)
will be sent for the current user. Notification settings are project-based,
which means that this returns the current user’s settings for every room.

```ts
const settings = await client.getNotificationSettings();

// { email: { thread: true, ... }, slack: { thread: false, ... }, ... }
console.log(settings);
```

A user’s initial settings are set in the dashboard, and different kinds should
be enabled there. If no kind is enabled on the current channel, `null` will be
returned. For example, with the email channel:

```ts
const settings = await client.getNotificationSettings();

// { email: null, ... }
console.log(settings);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="settings" type="NotificationSettings">
    Current user’s notification settings.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### Client.updateNotificationSettings [@badge=Beta]

Updates the current user’s notification settings, which affects which
[notification webhook events](/docs/platform/webhooks#NotificationEvent) will be
sent for the current user. Notification settings are project-based, which means
that this modifies the current user’s settings in every room. Each notification
`kind` must first be enabled on your project’s notification dashboard page
before settings can be used.

```ts
const settings = await client.updateNotificationSettings({
  email: { thread: false },
  slack: { textMention: true },
});

// { email: { thread: false, ... }, slack: { textMention: true, ... }, ... }
console.log(settings);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="settings" type="NotificationSettings">
    Current user’s notification settings.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="settings" type="object" required>
    A deep partial object containing the notification settings to
    update. Custom notifications can be set too.

    <div className="-mt-2">
    ```js title="Examples" isCollapsable isCollapsed
    // You only need to pass partials
    await client.updateNotificationSettings({
      email: { thread: true },
    });

    // Enabling a custom notification on the slack channel
    await client.updateNotificationSettings({
      slack: { $myCustomNotification: true },
    });

    // Setting complex settings
    await client.updateNotificationSettings({
      email: {
        thread: true,
        textMention: false,
        $newDocument: true,
      },
        slack: {
        thread: false,
        $fileUpload: false,
      },
        teams: {
        thread: true,
      },
    });
    ```
    </div>

  </PropertiesListItem>
  <PropertiesListItem name="settings.email" type="NotificationChannelSettings">
    The email notification settings.
  </PropertiesListItem>
  <PropertiesListItem name="settings.slack" type="NotificationChannelSettings">
    The Slack notification settings.
  </PropertiesListItem>
  <PropertiesListItem name="settings.teams" type="NotificationChannelSettings">
    The Microsoft Teams notification settings.
  </PropertiesListItem>
  <PropertiesListItem name="settings.webPush" type="NotificationChannelSettings">
    The Web Push notification settings.
  </PropertiesListItem>
</PropertiesList>

## Storage

Each room contains Storage, a conflict-free data store that multiple users can
edit at the same time. When users make edits simultaneously, conflicts are
resolved automatically, and each user will see the same state. Storage is ideal
for storing permanent document state, such as shapes on a canvas, notes on a
whiteboard, or cells in a spreadsheet.

### Data structures

Storage provides three different conflict-free data structures, which you can
use to build your application. All structures are permanent and persist when all
users have left the room, unlike [Presence](/docs/ready-made-features/presence)
which is temporary.

- [`LiveObject`][] - Similar to JavaScript object. Use this for storing records
  with fixed key names and where the values don’t necessarily have the same
  types. For example, a `Person` with a `name: string` and an `age: number`
  field. If multiple clients update the same property simultaneously, the last
  modification received by the Liveblocks servers is the winner.

- [`LiveList`][] - An ordered collection of items synchronized across clients.
  Even if multiple users add/remove/move elements simultaneously, LiveList will
  solve the conflicts to ensure everyone sees the same collection of items.

- [`LiveMap`][] - Similar to a JavaScript Map. Use this for indexing values that
  all have the same structure. For example, to store an index of `Person` values
  by their name. If multiple users update the same property simultaneously, the
  last modification received by the Liveblocks servers is the winner.

### Typing Storage [#typing-storage]

To type the Storage values you receive, make sure to set your `Storage` type.

```ts file="liveblocks.config.ts"
import { LiveList } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    Storage: {
      animals: LiveList<{ name: string }>;
    };
  }
}
```

The type received in the callback will match the type passed. Learn more under
[typing your data](#typing-your-data).

```ts
const { root } = await room.getStorage();
const animals = root.get("animals");

const unsubscribe = room.subscribe(animals, (updatedAnimals) => {
  // LiveList<[{ name: "Fido" }, { name: "Felix" }]>
  console.log(updatedAnimals);
});
```

### Nesting data structures

All Storage data structures can be nested, allowing you to create complex trees
of conflict-free data.

```ts file="liveblocks.config.ts"
import { LiveObject, LiveList, LiveMap } from "@liveblocks/client";

type Person = LiveObject<{
  name: string;
  pets: LiveList<string>;
}>;

declare global {
  interface Liveblocks {
    Storage: {
      people: LiveMap<string, Person>;
    };
  }
}
```

```ts
import { LiveObject, LiveList, LiveMap } from "@liveblocks/client";

const pets = new LiveList(["Cat", "Dog"]);
const person = new LiveObject({ name: "Alicia", pets });
const people = new LiveMap();
people.set("alicia", person);

const { root } = await room.getStorage();
root.set(people);
```

<Banner title="Need help troubleshooting Storage?">

Get the [Liveblocks DevTools extension](/devtools) to develop and debug your
application as you build it.

</Banner>

### Room.getStorage

Get the room’s Storage asynchronously (returns a Promise). The promise will
resolve once the Storage’s root is loaded and available. The Storage’s root is
always a [`LiveObject`][].

```ts
const { root } = await room.getStorage();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="storage" type="{ root: LiveObject<TStorage> }">
    The room’s Storage structures. `root` is a `LiveObject`, and is the root of
    your Storage. Learn more about [typing Storage](#typing-storage).
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

## LiveObject

The `LiveObject` class is similar to a JavaScript object that is synchronized on
all clients. Use this for storing records with fixed key names and where the
values don’t necessarily have the same types. For example, a `Person` with
`name` and `age` fields. To add typing, read more under
[typing Storage](#typing-storage).

```ts
type Person = LiveObject<{
  name: string;
  age: number;
}>;
```

Keys are strings, and values can contain other Storage structures, or
JSON-serializable data. If multiple clients update the same property
simultaneously, the last modification received by the Liveblocks servers is the
winner.

### new LiveObject [#LiveObject.constructor]

Create an empty `LiveObject`

```ts
import { LiveObject } from "@liveblocks/client";

const object = new LiveObject();
```

Create a `LiveObject` with initial data.

```ts
import { LiveObject } from "@liveblocks/client";

const object = new LiveObject({ firstName: "Margaret", lastName: "Hamilton" });
```

<PropertiesList title="Returns">
  <PropertiesListItem name="LiveObject" type="LiveObject<L>">
    The newly created `LiveObject`.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="initialValue" type="L extends LsonObject" required>
    The initial value for the `LiveObject`. Can contain JSON-serializable data
    and other Liveblocks conflict-free data structures.
  </PropertiesListItem>
</PropertiesList>

#### Add a LiveObject to Storage

The Storage root is `LiveObject` itself, so you can use [`LiveObject.set`]() to
add a new property to your root. If you’ve [typed Storage](#typing-storage)
you’ll have type hints as you build.

```ts
import { LiveObject } from "@liveblocks/client";

const { root } = await room.getStorage();

const person = new LiveObject({ name: "Alicia" });
root.set("person", person);
```

### delete [#LiveObject.delete]

Delete a property from the `LiveObject`

```ts
const object = new LiveObject({ firstName: "Ada", lastName: "Lovelace" });
object.delete("lastName");

// { firstName: "Ada" }
object.toImmutable();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="key" type="string" required>
    The key of the property you’re deleting. If the property doesn’t exist,
    nothing will occur.
  </PropertiesListItem>
</PropertiesList>

### get [#LiveObject.get]

Get a property from the `LiveObject`.

```ts
const object = new LiveObject({ firstName: "Ada", lastName: "Lovelace" });

// "Ada"
object.get("firstName");
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value">
    The value of the property. Returns `undefined` if it doesn’t exist.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="key" type="string" required>
    The key of the property you’re getting.
  </PropertiesListItem>
</PropertiesList>

### set [#LiveObject.set]

Adds or updates a property with the specified key and a value.

```ts
const object = new LiveObject({ firstName: "Marie" });
object.set("lastName", "Curie");

// { firstName: "Ada", lastName: "Curie" }
object.toImmutable();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="key" type="string" required>
    The key of the property you’re setting.
  </PropertiesListItem>
  <PropertiesListItem name="value" type="LsonObject" required>
    The value of the property you’re setting. Can contain JSON-serializable data
    and other Liveblocks conflict-free data structures.
  </PropertiesListItem>
</PropertiesList>

### update [#LiveObject.update]

Adds or updates multiple properties at once. Nested changes to other Storage
types will not be applied.

```ts
const object = new LiveObject({ firstName: "Grace" });
object.update({ lastName: "Hopper", job: "Computer Scientist" });

// { firstName: "Grace", lastName: "Hopper", job: "Computer Scientist" }
object.toImmutable();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="value" type="LsonObject" required>
    The keys and values you’re updating. Can contain JSON-serializable data and
    other Liveblocks conflict-free data structures. Nested changes to other
    Storage types will not be applied.
  </PropertiesListItem>
</PropertiesList>

### clone [#LiveObject.clone]

Returns a deep copy of the `LiveObject` that can be inserted elsewhere in the
Storage tree.

```ts
const obj = new LiveObject(/* ... */);
root.set("a", obj);
root.set("b", obj.clone());
```

<PropertiesList title="Returns">
  <PropertiesListItem name="clonedStructure" type="LiveObject">
    The cloned `LiveObject`.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### toImmutable [#LiveObject.toImmutable]

Returns an immutable JavaScript object that is equivalent to the `LiveObject`.
Nested values will also be immutable. Calling this method multiple times has no
performance penalty. It will return the same cached immutable value as long as
its (nested) contents have not changed.

```ts
const liveObject = new LiveObject({
  firstName: "Grace",
  lastName: "Hopper",
  hobbies: new LiveList(["reading", "piano"]),
});

// {
//   firstName: "Grace",
//   lastName: "Hopper",
//   hobbies: ["reading", "piano"]
// }
liveObject.toImmutable();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="immutableStructure" type="object">
    Returns a JavaScript object in the shape of your data structure.
    `LiveObject` is converted to an object, `LiveMap` to a map, and `LiveList`
    to an array.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### toObject [#LiveObject.toObject]

<Banner title="Will be deprecated in the future">

Starting with 0.18, we recommend [`toImmutable`][] instead. It’s faster, cached,
and leads to fewer surprises.

</Banner>

Transform the `LiveObject` into a normal JavaScript object.

```ts
const liveObject = new LiveObject({ firstName: "Grace", lastName: "Hopper" });
liveObject.toObject();
// { firstName: "Grace", lastName: "Hopper" }
```

Please note that this method won’t recursively convert Live structures, which
may be surprising:

```ts
const liveObject = new LiveObject({
  animals: new LiveList(["🦁", "🦊", "🐵"]),
});
liveObject.toObject();
// { animals: <LiveList instance> } // ❗️
```

## LiveMap

The `LiveMap` class is similar to a
[JavaScript Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
that is synchronized on all clients. Use this for indexing values that all have
the same structure. For example, to store an index of `Person` values by their
name. To add typing, read more under [typing Storage](#typing-storage).

```ts
type Shapes = LiveMap<string, LiveObject<{ name: string }>>;
```

Keys are strings, and values can contain other Storage structures, or
JSON-serializable data. If multiple clients update the same property
simultaneously, the last modification received by the Liveblocks servers is the
winner.

### new LiveMap [#LiveMap.constructor]

Create an empty `LiveMap`.

```ts
const map = new LiveMap();
```

Create a `LiveMap` with initial data.

```ts
const map = new LiveMap([
  ["nimesh", "developer"],
  ["pierre", "designer"],
]);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="LiveMap" type="LiveMap<string, L>">
    The newly created `LiveMap`.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="initialValue"
    type="[string, L extends LsonObject][]"
    required
  >
    The initial value for the `LiveMap`. An array of tuples, each containing a
    key and a value. The values can contain JSON-serializable data and other
    Liveblocks conflict-free data structures.
  </PropertiesListItem>
</PropertiesList>

#### Add a LiveMap to Storage

The Storage root is a `LiveObject`, so you can create a new `LiveMap` then use
[`LiveObject.set`]() to add it to your root. If you’ve
[typed Storage](#typing-storage) you’ll have type hints as you build.

```ts
import { LiveMap } from "@liveblocks/client";

const { root } = await room.getStorage();

const people = new LiveMap([
  ["vincent", "engineer"],
  ["marc", "designer"],
]);
root.set("people", people);
```

### delete [#LiveMap.delete]

Removes the specified element by key. Returns true if an element existed and has
been removed, or false if the element does not exist.

```ts
const map = new LiveMap([
  ["nimesh", "developer"],
  ["pierre", "designer"],
]);

// true
map.delete("nimesh");

// Map { "pierre" => "designer" }
map.toImmutable();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="deleted" type="boolean">
    If the element existed and was removed.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="key" type="string" required>
    The key of the element you’re deleting. If the element doesn’t exist,
    nothing will occur.
  </PropertiesListItem>
</PropertiesList>

### entries [#LiveMap.entries]

Returns a new
[Iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
object that contains the `[key, value]` pairs for each element.

```ts
for (const [key, value] of map.entries()) {
  // Iterate over all the keys and values of the map
}
```

<Banner title="Iteration with TypeScript">

If your TypeScript project targets <code>es5</code> or lower, you’ll need to
enable the <code>--downlevelIteration</code> option to use this API.

</Banner>

<PropertiesList title="Returns">
  <PropertiesListItem name="iterator" type="IterableIterator<[string, L]>">
    A new Iterator object for the `LiveMap`, containing the `[key, value]` pairs
    for each element.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### forEach [#LiveMap.forEach]

Executes a provided function once per each key/value pair in the Map object, in
insertion order.

```ts
const map = new LiveMap([
  ["nimesh", "developer"],
  ["pierre", "designer"],
]);

// "developer", "designer"
map.forEach((value, key, liveMap) => console.log(value));
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    detailedType="(value: L, key: string, map: LiveMap<string, L>) => void"
    required
  >
    A callback for each entry. The callback is passed the current `value`,
    `key`, and the `LiveMap`. Return values are ignored.
  </PropertiesListItem>
</PropertiesList>

### get [#LiveMap.get]

Returns a specified element from the `LiveMap`. Returns `undefined` if the key
can’t be found.

```ts
const map = new LiveMap([
  ["nimesh", "developer"],
  ["pierre", "designer"],
]);

// "developer"
map.get("nimesh");

// undefined
map.get("alicia");
```

<PropertiesList title="Returns">
  <PropertiesListItem name="value" type="L | undefined">
    The value of the entry. Returns `undefined` if it doesn’t exist.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="key" type="string" required>
    The key of the entry you’re getting.
  </PropertiesListItem>
</PropertiesList>

### has [#LiveMap.has]

Returns a boolean indicating whether an element with the specified key exists or
not.

```ts
const map = new LiveMap([
  ["nimesh", "developer"],
  ["pierre", "designer"],
]);

// true
map.has("nimesh");

// false
map.has("alicia");
```

<PropertiesList title="Returns">
  <PropertiesListItem name="exists" type="boolean">
    Whether the entry exists.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="key" type="string" required>
    The key of the entry you’re getting.
  </PropertiesListItem>
</PropertiesList>

### keys [#LiveMap.keys]

Returns a new
[Iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
object that contains the keys for each element.

```ts
for (const key of map.keys()) {
  // Iterate over all the keys and values of the map
}
```

<Banner title="Iteration with TypeScript">

If your TypeScript project targets <code>es5</code> or lower, you’ll need to
enable the <code>--downlevelIteration</code> option to use this API.

</Banner>

<PropertiesList title="Returns">
  <PropertiesListItem name="iterator" type="IterableIterator<string>">
    A new Iterator object for the `LiveMap`, containing the keys of each entry.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### set [#LiveMap.set]

Adds or updates an element with a specified key and a value.

```ts
const map = new LiveMap();
map.set("vincent", "engineer");

// Map { "vincent" => "engineer" }
map.toImmutable();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="key" type="string" required>
    The key of the entry you’re setting.
  </PropertiesListItem>
  <PropertiesListItem name="value" type="LsonObject" required>
    The value of the entry you’re setting. Can contain JSON-serializable data
    and other Liveblocks conflict-free data structures.
  </PropertiesListItem>
</PropertiesList>

### size [#LiveMap.size]

Returns the number of elements in the `LiveMap`.

```ts
const map = new LiveMap([
  ["nimesh", "developer"],
  ["pierre", "designer"],
]);

// 2
map.size;
```

<PropertiesList title="Returns">
  <PropertiesListItem name="size" type="number">
    The number of entries in the `LiveMap`
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem>_N/A_</PropertiesListItem>
</PropertiesList>

### values [#LiveMap.values]

Returns a new
[Iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
object that contains the values for each element.

```ts
for (const value of map.values()) {
  // Iterate over all the values of the map
}
```

<Banner title="Iteration with TypeScript">

If your TypeScript project targets <code>es5</code> or lower, you’ll need to
enable the <code>--downlevelIteration</code> option to use this API.

</Banner>

<PropertiesList title="Returns">
  <PropertiesListItem name="iterator" type="IterableIterator<L>">
    A new Iterator object for the `LiveMap`, containing the values of each
    entry.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### clone [#LiveMap.clone]

Returns a deep copy of the `LiveMap` that can be inserted elsewhere in the
Storage tree.

```ts
const map = new LiveMap(/* ... */);
root.set("a", map);
root.set("b", map.clone());
```

<PropertiesList title="Returns">
  <PropertiesListItem name="clonedStructure" type="LiveMap">
    The cloned `LiveMap`.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### toImmutable [#LiveMap.toImmutable]

Returns an immutable ES6 Map that is equivalent to the `LiveMap`. Nested values
will also be immutable. Calling this method multiple times has no performance
penalty. It will return the same cached immutable value as long as its (nested)
contents have not changed.

```ts
const map = new LiveMap([
  ["florent", new LiveObject({ role: "engineer" })],
  ["marc", new LiveObject({ role: "designer" })],
]);

// Map {
//   "florent" => { role: "engineer" },
//   "marc" => { role: "designer" },
// }
map.toImmutable();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="immutableStructure" type="object">
    Returns a JavaScript object in the shape of your data structure. `LiveMap`
    is converted to a map, `LiveObject` to an object, and `LiveList` to an
    array.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

## LiveList

The `LiveList` class represents an ordered collection of items that is
synchronized across clients. To add typing, read more under
[typing Storage](#typing-storage).

```ts
type Names = LiveList<string>;
```

Items can contain other Storage structures, or JSON-serializable data.

### new LiveList [#LiveList.constructor]

Create an empty `LiveList`.

```ts
const list = new LiveList();
```

Create a `LiveList` with initial data.

```ts
const list = new LiveList(["adrien", "jonathan"]);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="LiveList" type="LiveList<L>">
    The newly created `LiveList`.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="initialValue"
    type="Array<L extends LsonObject>"
    required
  >
    The initial array of values for the `LiveList`. Can contain
    JSON-serializable data and other Liveblocks conflict-free data structures.
  </PropertiesListItem>
</PropertiesList>

### clear [#LiveList.clear]

Removes all the elements.

```ts
const list = new LiveList(["adrien", "jonathan"]);
list.clear();

// []
list.toImmutable();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### delete [#LiveList.delete]

Deletes the element living at the specified index locally. If the index doesn't
exist, an `Error` is thrown.

```ts
const list = new LiveList(["adrien", "jonathan"]);
list.delete(0);

// ["jonathan"]
list.toImmutable();
```

This operation uses ID-based semantics, not position-based. When called, it
reads the item at the specified index from the local state, then sends a "delete
item with ID X" instruction to the server.

If clients A and B both see a LiveList containing `["foo", "bar"]`, and client A
calls `.insert("qux", 0)`, while client B simultaneously calls `.delete(0)`, the
end result will always be `["qux", "bar"]` on both clients, and never
`["foo", "bar"]`.

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="index" type="number" required>
    The index of the property you’re deleting. If the property doesn’t exist, an
    `Error` is thrown.
  </PropertiesListItem>
</PropertiesList>

### every [#LiveList.every]

Tests whether all elements pass the test implemented by the provided function.
Returns true if the predicate function returns a truthy value for every element.
Otherwise, false.

```ts
const list = new LiveList([0, 2, 4]);

// true
list.every((i) => i % 2 === 0);

list.push(5);

// false
list.every((i) => i % 2 === 0);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="isEvery" type="boolean">
    Whether all elements pass the test implemented by the provided function.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    type="(value: L, index: number) => unknown"
    required
  >
    A function to execute for each item in the array. It should return a truthy
    value to indicate the element passes the test, and a falsy value otherwise.
    The function is passed the `value` of the item and its current `index`.
  </PropertiesListItem>
</PropertiesList>

### filter [#LiveList.filter]

Creates an array with all elements that pass the test implemented by the
provided function.

```ts
const list = new LiveList([0, 1, 2, 3, 4]);

// [0, 2, 4]
list.filter((i) => i % 2 === 0);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="filteredArray" type="L[]">
    An array containing each item of the `LiveList` that passed the test
    implemented by the provided function.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    type="(value: L, index: number) => unknown"
    required
  >
    A function to execute for each item in the array. It should return a truthy
    value to indicate the element passes the test, and a falsy value otherwise.
    The function is passed the `value` of the item and its current `index`.
  </PropertiesListItem>
</PropertiesList>

### find [#LiveList.find]

Returns the first element that satisfies the provided testing function. If no
item passes the test, `undefined` is returned.

```ts
const list = new LiveList(["apple", "lemon", "tomato"]);

// "lemon"
list.find((value, index) => value.startsWith("l"));
```

<PropertiesList title="Returns">
  <PropertiesListItem name="item" type="L | undefined">
    The item that has been found. If no item passes the test, `undefined` is
    returned.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    type="(value: L, index: number) => unknown"
    required
  >
    A function to execute for each item in the array. It should return a truthy
    value to indicate the element passes the test, and a falsy value otherwise.
    The function is passed the `value` of the item and its current `index`.
  </PropertiesListItem>
</PropertiesList>

### findIndex [#LiveList.findIndex]

Returns the index of the first element in the `LiveList` that satisfies the
provided testing function. If no item passes the test, `-1` is returned.

```ts
const list = new LiveList(["apple", "lemon", "tomato"]);

// 1
list.findIndex((value, index) => value.startsWith("l"));
```

<PropertiesList title="Returns">
  <PropertiesListItem name="index" type="number">
    The index of the item that has been found. If no item passes the test, `-1`
    is returned.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    type="(value: L, index: number) => unknown"
    required
  >
    A function to execute for each item in the array. It should return a truthy
    value to indicate the element passes the test, and a falsy value otherwise.
    The function is passed the `value` of the item and its current `index`.
  </PropertiesListItem>
</PropertiesList>

### forEach [#LiveList.forEach]

Executes a provided function once for each element.

```ts
const list = new LiveList(["adrien", "jonathan"]);

// "adrien", "jonathan"
list.forEach((item) => console.log(item));
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    detailedType="(value: L, index: number) => void"
    required
  >
    A callback for each item. The callback is passed the current `value` and
    `index`. Return values are ignored.
  </PropertiesListItem>
</PropertiesList>

### get [#LiveList.get]

Get the element at the specified index. Returns `undefined` if the index doesn’t
exist.

```ts
const list = new LiveList(["adrien", "jonathan"]);

// "jonathan"
list.get(1);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="item" type="L | undefined">
    The value of the item at the index. Returns `undefined` if it doesn’t exist.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="index" type="number" required>
    The index of the item you’re getting.
  </PropertiesListItem>
</PropertiesList>

### indexOf [#LiveList.indexOf]

Returns the first index at which a given element can be found in the `LiveList`.
Returns `-1` if it is not present.

```ts
const list = new LiveList(["adrien", "jonathan"]);

// 1
list.indexOf("jonathan");

// undefined
list.indexOf("chris");
```

<PropertiesList title="Returns">
  <PropertiesListItem name="index" type="number">
    The index of the item. Returns `-1` if it doesn’t exist.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="searchElement" type="L" required>
    The item you’re locating.
  </PropertiesListItem>
  <PropertiesListItem name="index" type="number">
    The index to start the search at.
  </PropertiesListItem>
</PropertiesList>

### insert [#LiveList.insert]

Inserts one element at a specified index. Throws an `Error` if the index is out
of bounds.

```ts
const list = new LiveList(["adrien", "jonathan"]);

list.insert("chris", 1);

// ["adrien", "chris", "jonathan"]
list.toImmutable();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="value" type="L extends LsonObject" required>
    The value of the item you’re inserting.
  </PropertiesListItem>
  <PropertiesListItem name="index" type="number" required>
    The index to insert the item into.
  </PropertiesListItem>
</PropertiesList>

### lastIndexOf [#LiveList.lastIndexOf]

Returns the last index at which a given element can be found in the `LiveList`,
or -1 if it is not present. The `LiveList` is searched backwards, starting at
fromIndex. Returns `-1` if it is not present.

```ts
const list = new LiveList(["adrien", "jonathan", "adrien"]);

// 2
list.indexOf("adrien");

// undefined
list.indexOf("chris");
```

<PropertiesList title="Returns">
  <PropertiesListItem name="index" type="number">
    The index of the item. Returns `-1` if it doesn’t exist.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="searchElement" type="L" required>
    The item you’re locating.
  </PropertiesListItem>
  <PropertiesListItem name="index" type="number">
    The index at which to start searching backwards.
  </PropertiesListItem>
</PropertiesList>

### length [#LiveList.length]

Returns the number of elements.

```ts
const list = new LiveList(["adrien", "jonathan"]);

// 2
list.length;
```

<PropertiesList title="Returns">
  <PropertiesListItem name="length" type="number">
    The number of items in the `LiveList`.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem>_N/A_</PropertiesListItem>
</PropertiesList>

### map [#LiveList.map]

Creates an array populated with the results of calling a provided function on
every element.

```ts
const list = new LiveList(["apple", "lemon", "tomato"]);

// ["APPLE", "LEMON", "TOMATO"]
list.map((value, index) => value.toUpperCase());
```

<PropertiesList title="Returns">
  <PropertiesListItem name="array">
    The array of each item has been transformed by the callback function.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    detailedType="(value: L, index: number) => void"
    required
  >
    A callback for each item. The callback is passed the current `value` and
    `index`. Return values are used in the returned array.
  </PropertiesListItem>
</PropertiesList>

### move [#LiveList.move]

Moves one element at a specified index.

```ts
const list = new LiveList(["adrien", "chris", "jonathan"]);

list.move(2, 0);

// ["jonathan", "adrien", "chris"]
list.toImmutable();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="index" type="number" required>
    The index of the item to move.
  </PropertiesListItem>
  <PropertiesListItem name="targetIndex" type="number" required>
    The index where the element should be after moving.
  </PropertiesListItem>
</PropertiesList>

### push [#LiveList.push]

Adds one element to the end of the `LiveList`.

```ts
const list = new LiveList(["adrien", "jonathan"]);

list.push("chris");

// ["adrien", "jonathan", "chris"]
list.toImmutable();
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="index" type="L" required>
    The item to add to the end of the `LiveList`.
  </PropertiesListItem>
</PropertiesList>

### set [#LiveList.set]

Replace one element at the specified index.

```ts
const list = new LiveList(["adrien", "jonathan"]);

list.set(1, "chris");

// equals ["adrien", "chris"]
list.toImmutable();
```

### some [#LiveList.some]

Tests whether at least one element in the `LiveList` passes the test implemented
by the provided function.

```ts
const list = new LiveList(["apple", "lemon", "tomato"]);

// true
list.some((value, index) => value.startsWith("l"));

// false
list.some((value, index) => value.startsWith("x"));
```

<PropertiesList title="Returns">
  <PropertiesListItem name="areSome" type="boolean">
    Whether any elements pass the test implemented by the provided function.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    type="(value: L, index: number) => unknown"
    required
  >
    A function to execute for each item in the array. It should return a truthy
    value to indicate the element passes the test, and a falsy value otherwise.
    The function is passed the `value` of the item and its current `index`.
  </PropertiesListItem>
</PropertiesList>

### clone [#LiveList.clone]

Returns a deep copy of the `LiveList` that can be inserted elsewhere in the
Storage tree.

```ts
const list = new LiveList(/* ... */);
root.set("a", list);
root.set("b", list.clone());
```

<PropertiesList title="Returns">
  <PropertiesListItem name="clonedStructure" type="LiveList">
    The cloned `LiveList`.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### toImmutable [#LiveList.toImmutable]

Returns an immutable JavaScript array that is equivalent to the `LiveList`.
Nested values will also be immutable. Calling this method multiple times has no
performance penalty. It will return the same cached immutable value as long as
its (nested) contents have not changed.

```ts
const list = new LiveList([
  new LiveObject({ name: "Olivier" }),
  new LiveObject({ name: "Vincent" }),
]);

// [
//   { name: "Olivier" },
//   { name: "Vincent" },
// ]
list.toImmutable();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="immutableStructure" type="object">
    Returns a JavaScript object in the shape of your data structure. `ListList`
    is converted to an array, `LiveObject` to an object, and `LiveMap` to a map.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### toArray [#LiveList.toArray]

<Banner title="Will be deprecated in the future">

Starting with 0.18, we recommend [`toImmutable`][] instead. It’s faster, cached,
and leads to fewer surprises.

</Banner>

Transforms the `LiveList` into a normal JavaScript array.

```ts
const list = new LiveList(["🦁", "🦊", "🐵"]);
list.toArray();
// ["🦁", "🦊", "🐵"]
```

Please note that this method won’t recursively convert Live structures, which
may be surprising:

```ts
const list = new LiveList([
  new LiveObject({ firstName: "Grace", lastName: "Hopper" }),
]);
list.toArray();
// [ <LiveObject instance> ]  // ❗️
```

## Resolvers

### invalidateUsers

`client.resolvers.invalidateUsers` can be used to invalidate some or all users
that were previously cached by [`resolveUsers`](#createClientResolveUsers).

It can be used when updating the current user’s avatar for example, to instantly
refresh the user data everywhere without having to perform a page reload.

```tsx
// Invalidate all users
client.resolvers.invalidateUsers();

// Only invalidate "user-0" and "user-1"
client.resolvers.invalidateUsers(["user-0", "user-1"]);
```

### invalidateRoomsInfo

`client.resolvers.invalidateRoomsInfo` can be used to invalidate some or all
rooms that were previously cached by
[`resolveRoomsInfo`](#createClientResolveRoomsInfo).

It can be used when updating a room’s name for example, to instantly refresh the
room info everywhere without having to perform a page reload.

```tsx
// Invalidate all rooms
client.resolvers.invalidateRoomsInfo();

// Only invalidate "room-0" and "room-1"
client.resolvers.invalidateRoomsInfo(["room-0", "room-1"]);
```

### invalidateGroupsInfo

`client.resolvers.invalidateGroupsInfo` can be used to invalidate some or all
groups that were previously cached by
[`resolveGroupsInfo`](#createClientResolveGroupsInfo).

It can be used when updating a group’s name for example, to instantly refresh
the group info everywhere without having to perform a page reload.

```tsx
// Invalidate all groups
client.resolvers.invalidateGroupsInfo();

// Only invalidate "group-0" and "group-1"
client.resolvers.invalidateGroupsInfo(["group-0", "group-1"]);
```

### invalidateMentionSuggestions

`client.resolvers.invalidateMentionSuggestions` can be used to invalidate all
mention suggestions that were previously cached by
[`resolveMentionSuggestions`](#createClientResolveMentionSuggestions).

It can be used when updating a room’s list of users for example, to prevent
creating out-of-date mentions without having to perform a page reload.

```tsx
// Invalidate all mention suggestions
client.resolvers.invalidateMentionSuggestions();
```

## Utilities

### getMentionsFromCommentBody [#get-mentions-from-comment-body]

Returns an array of mentions from a `CommentBody` (found under `comment.body`).

```ts
import { getMentionsFromCommentBody } from "@liveblocks/client";

const mentions = getMentionsFromCommentBody(comment.body);
```

An optional second argument can be used to filter the returned mentions. By
default, if it’s not provided, all mentions are returned, including future
mention kinds (e.g. group mentions in the future).

```tsx
// All mentions (same as `getMentionsFromCommentBody(commentBody)`)
getMentionsFromCommentBody(commentBody);

// Only user mentions with an ID of "123"
getMentionsFromCommentBody(
  commentBody,
  (mention) => mention.kind === "user" && mention.id === "123"
);

// Only mentions with an ID which starts with "prefix:"
getMentionsFromCommentBody(commentBody, (mention) => (
  mention.id.startsWith("prefix:")
);
```

Here’s an example with a custom `CommentBody`.

```ts
import { CommentBody, getMentionsFromCommentBody } from "@liveblocks/client";

// Create a custom `CommentBody`
const commentBody: CommentBody = {
  version: 1,
  content: [
    {
      type: "paragraph",
      children: [
        { text: "Hello " },
        { type: "mention", id: "chris@example.com" },
      ],
    },
  ],
};

// Get the mentions inside the comment’s body
const mentions = getMentionsFromCommentBody(commentBody);

// [{ kind: "user", id: "chris@example.com" }]
console.log(mentions);
```

<Banner title="Also available from @liveblocks/node">

If you’d like to use this on the server side, it’s also available from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node#get-mentions-from-comment-body).

</Banner>

### stringifyCommentBody [#stringify-comment-body]

Used to convert a `CommentBody` (found under `comment.body`) into either a plain
string, Markdown, HTML, or a custom format.

```ts
import { stringifyCommentBody } from "@liveblocks/client";

const stringComment = await stringifyCommentBody(comment.body);

// "Hello marc@example.com from https://liveblocks.io"
console.log(stringComment);
```

A number of options are available.

```ts
import { stringifyCommentBody } from "@liveblocks/client";

const stringComment = await stringifyCommentBody(comment.body, {
  // Optional, convert to specific format, "plain" (default) | "markdown" | "html"
  format: "markdown",

  // Optional, supply a separator to be used between paragraphs
  separator: `\n\n`,

  // Optional, override any elements in the CommentBody with a custom string
  elements: {
    // Optional, override the `paragraph` element
    paragraph: ({ element, children }) => `<p>${children}</p>`,

    // Optional, override the `text` element
    text: ({ element }) =>
      element.bold ? `<strong>${element.text}</strong>` : `${element.text}`,

    // Optional, override the `link` element
    link: ({ element, href }) =>
      `<a href="${href}" target="_blank">${element.url}</a>`,

    // Optional, override the `mention` element.
    // `user` and `group` are the optional data returned from `resolveUsers` and `resolveGroupsInfo`
    mention: ({ element, user, group }) =>
      `<a href="${user?.profileUrl ?? group?.settingsUrl ?? "#"}">${
        element.id
      }</a>`,
  },

  // Optional, get your user’s names and info from their ID to be displayed in mentions
  async resolveUsers({ userIds }) {
    const usersData = await __getUsersFromDB__(userIds);

    return usersData.map((userData) => ({
      // Name is inserted into the output instead of a user’s ID
      name: userData.name,

      // Custom formatting in `elements.mention` allows custom properties to be used
      profileUrl: userData.profileUrl,
    }));
  },

  // Optional, get your group’s names and info from their ID to be displayed in mentions
  async resolveGroupsInfo({ groupIds }) {
    const groupsData = await __getGroupsFromDB__(groupIds);

    return groupsData.map((groupData) => ({
      // Name is inserted into the output instead of a group’s ID
      name: groupData.name,

      // Custom formatting in `elements.mention` allows custom properties to be used
      settingsUrl: groupData.settingsUrl,
    }));
  },
});
```

<Banner title="Also available from @liveblocks/node">

If you’d like to use this on the server side, it’s also available from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node#stringify-comment-body).

</Banner>

#### Formatting examples

Here are a number of different formatting examples derived from the same
`CommentBody`.

```ts
// "Hello marc@example.com from https://liveblocks.io"
await stringifyCommentBody(comment.body);

// "Hello @Marc from https://liveblocks.io"
await stringifyCommentBody(comment.body, {
  resolveUsers({ userIds }) {
    return [{ name: "Marc" }];
  },
});

// "**Hello** @Marc from [https://liveblocks.io](https://liveblocks.io)"
await stringifyCommentBody(comment.body, {
  format: "markdown",

  resolveUsers() {
    return [{ name: "Marc" }];
  },
});

// "<b>Hello</b> <span data-mention>@Marc</span> from
// <a href="https://liveblocks.io">https://liveblocks.io</a>"
await stringifyCommentBody(comment.body, {
  format: "html",

  resolveUsers() {
    return [{ name: "Marc" }];
  },
});

// "<b>Hello</b> <a href="https://example.com" data-id="marc@example.com">@Marc</a> from
// <a href="https://liveblocks.io">https://liveblocks.io</a>"
await stringifyCommentBody(comment.body, {
  format: "html",

  mention: ({ element, user }) =>
    `<a href="${user.profileUrl}" data-id="${element.id}">${user.name}</a>`,

  resolveUsers() {
    return [{ name: "Marc", profileUrl: "https://example.com" }];
  },
});
```

## TypeScript

### Typing your data

It’s possible to have automatic types flow through your application by defining
a global `Liveblocks` interface. We recommend doing this in a
`liveblocks.config.ts` file in the root of your app, so it’s easy to keep track
of your types. Each type (`Presence`, `Storage`, etc.), is optional, but it’s
recommended to make use of them.

```ts file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    // Each user’s Presence
    Presence: {};

    // The Storage tree for the room
    Storage: {};

    UserMeta: {
      id: string;
      // Custom user info set when authenticating with a secret key
      info: {};
    };

    // Custom events
    RoomEvent: {};

    // Custom metadata set on threads
    ThreadMetadata: {};

    // Custom metadata set on comments
    CommentMetadata: {};

    // Custom room info set with resolveRoomsInfo
    RoomInfo: {};

    // Custom group info set with resolveGroupsInfo
    GroupInfo: {};

    // Custom activities data for custom notification kinds
    ActivitiesData: {};
  }
}

// Necessary if you have no imports/exports
export {};
```

Here are some example values that might be used.

```ts file="liveblocks.config.ts"
import { LiveList } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    // Each user’s Presence
    Presence: {
      // Example, real-time cursor coordinates
      cursor: { x: number; y: number };
    };

    // The Storage tree for the room
    Storage: {
      // Example, a conflict-free list
      animals: LiveList<string>;
    };

    UserMeta: {
      id: string;
      // Custom user info set when authenticating with a secret key
      info: {
        // Example properties
        name: string;
        avatar: string;
      };
    };

    // Custom events
    // Example has two events, using a union
    RoomEvent: { type: "PLAY" } | { type: "REACTION"; emoji: "🔥" };

    // Custom metadata set on threads
    ThreadMetadata: {
      // Example, attaching coordinates to a thread
      x: number;
      y: number;
    };

    // Custom metadata set on comments
    CommentMetadata: {
      // Example, attaching a tag and a spam flag to a comment
      tag: string;
      spam: boolean;
    };

    // Custom room info set with resolveRoomsInfo
    RoomInfo: {
      // Example, rooms with a title and url
      title: string;
      url: string;
    };

    // Custom group info set with resolveGroupsInfo
    GroupInfo: {
      // Example, groups with a name and a badge
      name: string;
      badge: string;
    };

    // Custom activities data for custom notification kinds
    ActivitiesData: {
      // Example, a custom $alert kind
      $alert: {
        title: string;
        message: string;
      };
    };
  }
}

// Necessary if you have no imports/exports
export {};
```

### Typing with client.enter

Before Liveblocks 2.0, it was recommended to type your data by passing
`Presence`, `Storage`, `UserMeta`, and `RoomEvents` types to
[`client.enterRoom`][]. This is no longer
[the recommended method](#Typing-your-data) for setting up Liveblocks, but it
can still be helpful, for example you can use `client.enter` multiple times to
create different room types, each with their own correctly typed hooks.

```ts
import { LiveList } from "@liveblocks/client";

// Each user’s Presence
type Presence = {
  cursor: { x: number; y: number };
};

// The Storage tree for the room
type Storage = {
  animals: LiveList<string>;
};

// User information set when authenticating with a secret key
type UserMeta = {
  id: string;
  info: {
    // Custom properties, corresponds with userInfo
  };
};

// Custom events that can be broadcast, use a union for multiple events
type RoomEvent = {
  type: "REACTION";
  emoji: "🔥";
};

const { room, leave } = client.enterRoom<
  Presence,
  Storage,
  UserMeta,
  RoomEvent
>("my-room-id");
```

You can also pass types to
[`client.getRoom`](/docs/api-reference/liveblocks-client#Client.getRoom).

```ts
const { room, leave } = client.getRoom<Presence, Storage, UserMeta, RoomEvent>(
  "my-room-id"
);
```

### ToolDefinition

Type definition for AI tools that can be executed by the AI. This type
represents the structure of a tool that can be registered and used in AI
interactions.

```ts
type ToolDefinition<TArgs = any, TResult = any> = {
  description: string;
  parameters: JSONSchema7;
  execute: (
    args: TArgs,
    context: ToolExecutionContext
  ) => Promise<{ data: TResult }>;
  render: (props: ToolRenderProps<TArgs, TResult>) => ReactNode;
};
```

<PropertiesList title="Properties">
  <PropertiesListItem name="description" type="string">
    A clear description of what the tool does. Used by AI to understand when to
    call this tool.
  </PropertiesListItem>
  <PropertiesListItem name="parameters" type="JSONSchema7">
    JSON Schema defining the tool’s input parameters. The AI will validate
    arguments against this schema.
  </PropertiesListItem>
  <PropertiesListItem name="execute" type="function">
    Async function that performs the tool’s action. Receives validated arguments
    and execution context, returns structured data.
  </PropertiesListItem>
  <PropertiesListItem name="render" type="function">
    React component function that renders the tool’s UI during different
    execution stages.
  </PropertiesListItem>
</PropertiesList>

### AiKnowledgeSource

Type definition for knowledge sources that provide contextual information to AI.
Knowledge sources help the AI understand your application’s current state and
make more informed responses.

```ts
type AiKnowledgeSource = {
  description: string;
  value: string;
};
```

<PropertiesList title="Properties">
  <PropertiesListItem name="description" type="string">
    A clear description of what this knowledge represents (e.g., "Current user’s
    profile", "Application settings").
  </PropertiesListItem>
  <PropertiesListItem
    name="value"
    type="string | object | array | number | boolean | null"
  >
    The knowledge content. Can be any JSON-compatible format that provides
    context to the AI.
  </PropertiesListItem>
</PropertiesList>

Knowledge sources can be registered using
[`RegisterAiKnowledge`](/docs/api-reference/liveblocks-react#RegisterAiKnowledge)
or passed directly to [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat)
via the `knowledge` prop.

```ts
// Example knowledge sources
const userKnowledge: AiKnowledgeSource = {
  description: "Current user information",
  value: { name: "John Doe", role: "admin" },
};

const appStateKnowledge: AiKnowledgeSource = {
  description: "Current application state",
  value: "The user is currently editing a document in dark mode",
};
```

### User [#user-type]

`User` is a type that’s returned by [`room.getSelf`][], [`room.getOthers`][],
and other functions. Some of its values are set when
[typing your room](#Typing-your-data), here are some example values:

```ts file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    // Each user’s Presence
    // +++
    Presence: {
      cursor: { x: number; y: number };
    };
    // +++

    UserMeta: {
      id: string;
      // Custom user info set when authenticating with a secret key
      // +++
      info: {
        name: string;
        avatar: string;
      };
      // +++
    };
  }
}
```

```ts
const { room, leave } = client.enterRoom("my-room-id");

// {
//   connectionId: 52,
//   +++
//   presence: {
//     cursor: { x: 263, y: 786 },
//   },
//   +++
//   id: "mislav.abha@example.com",
//   +++
//   info: {
//     name: "Mislav Abha",
//     avatar: "/mislav.png",
//   },
//   +++
//   canWrite: true,
//   canComment: true,
// }
const user = room.getSelf();
```

<PropertiesList title="Properties">
  <PropertiesListItem name="connectionId" type="number">
    The connection ID of the User. It is unique and increments with every new
    connection.
  </PropertiesListItem>
  <PropertiesListItem name="id" type={`UserMeta["id"]`}>
    The ID of the User that has been set in the authentication endpoint. Useful
    to get additional information about the connected user.
  </PropertiesListItem>
  <PropertiesListItem name="info" type={`UserMeta["info"]`}>
    Additional user information that has been set in the authentication
    endpoint.
  </PropertiesListItem>
  <PropertiesListItem name="presence" type={`TPresence`}>
    The user’s Presence data.
  </PropertiesListItem>
  <PropertiesListItem name="canWrite" type="boolean">
    `true` if the user can mutate the Room’s Storage and/or YDoc, `false` if
    they can only read but not mutate it. Set via your [room
    permissions](/docs/authentication#Room-permissions).
  </PropertiesListItem>
  <PropertiesListItem name="canComment" type="boolean">
    `true` if the user can leave a comment in the room, `false` if they can only
    read comments but not leave them. Set via your [room
    permissions](/docs/authentication#Room-permissions).
  </PropertiesListItem>
</PropertiesList>

[`atob`]: https://developer.mozilla.org/en-US/docs/Web/API/atob
[`base-64`]: https://www.npmjs.com/package/base-64
[`client.enterroom`]: /docs/api-reference/liveblocks-client#Client.enterRoom
[`client.getroom`]: /docs/api-reference/liveblocks-client#Client.getRoom
[`createclient`]: /docs/api-reference/liveblocks-client#createClient
[`fetch`]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
[`livelist`]: /docs/api-reference/liveblocks-client#LiveList
[`livemap`]: /docs/api-reference/liveblocks-client#LiveMap
[`liveobject`]: /docs/api-reference/liveblocks-client#LiveObject
[`lostconnectiontimeout`]:
  /docs/api-reference/liveblocks-client#createClientLostConnectionTimeout
[`toimmutable`]: /docs/api-reference/liveblocks-client#LiveObject.toImmutable
[`node-fetch`]: https://npmjs.com/package/node-fetch
[`room.broadcastevent`]:
  /docs/api-reference/liveblocks-client#Room.broadcastEvent
[`room.getstorage`]: /docs/api-reference/liveblocks-client#Room.getStorage
[`room.reconnect`]: /docs/api-reference/liveblocks-client#Room.reconnect
[`room.getself`]: /docs/api-reference/liveblocks-client#Room.getSelf
[`room.getothers`]: /docs/api-reference/liveblocks-client#Room.getOthers
[`room.getstorage`]: /docs/api-reference/liveblocks-client#Room.getStorage
[`room.getsubscriptionsettings`]:
  /docs/api-reference/liveblocks-client#Room.getSubscriptionSettings
[`room.updatesubscriptionsettings`]:
  /docs/api-reference/liveblocks-client#Room.updateSubscriptionSettings
[`room.history`]: /docs/api-reference/liveblocks-client#Room.history
[`room.subscribe("event")`]:
  /docs/api-reference/liveblocks-client#Room.subscribe.event
[`room.subscribe("status")`]:
  /docs/api-reference/liveblocks-client#Room.subscribe.status
[`room.subscribe("lost-connection")`]:
  /docs/api-reference/liveblocks-client#Room.subscribe.lost-connection
[`room.subscribe("storage-status")`]:
  /docs/api-reference/liveblocks-client#Room.subscribe.storage-status
[`room.updatepresence`]:
  /docs/api-reference/liveblocks-client#Room.updatePresence
[`websocket`]: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
[`ws`]: https://www.npmjs.com/package/ws
[connection status example]:
  https://liveblocks.io/examples/connection-status/nextjs
[3.4]: https://github.com/liveblocks/liveblocks/releases/tag/v3.4.0

---
meta:
  title: "@liveblocks/emails"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/emails package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/emails` provides a set of functions that simplifies sending styled
emails with [Notifications](/docs/ready-made-features/notifications) and
[webhooks](/docs/platform/webhooks). This library is only intended for use in
your Node.js back end.

## Requirements

`@liveblocks/emails` requires the
[`@liveblocks/node`](/docs/api-reference/liveblocks-node) package to be
installed and for [`react`](https://react.dev/) to be a peer dependency in your
project.

## Setup

This package exposes functions that enable easy creation of styled emails with
React and HTML. Each method is designed to be used with our
[webhooks](/docs/platform/webhooks) which means you must
[set them up](/docs/guides/how-to-test-webhooks-on-localhost) first. Webhooks
require an API endpoint in your application, and this is typically what they
will look like.

```tsx title="Next.js route handler for webhooks"
import { isThreadNotificationEvent, WebhookHandler } from "@liveblocks/node";
import { Liveblocks } from "@liveblocks/node";
// +++
import { prepareThreadNotificationEmailAsReact } from "@liveblocks/emails";
// +++

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

const webhookHandler = new WebhookHandler(
  process.env.LIVEBLOCKS_WEBHOOK_SECRET_KEY as string
);

export async function POST(request: Request) {
  const body = await request.json();
  const headers = request.headers;

  // Verify if this is a real webhook request
  let event;
  try {
    event = webhookHandler.verifyRequest({
      headers: headers,
      rawBody: JSON.stringify(body),
    });
  } catch (err) {
    console.error(err);
    return new Response("Could not verify webhook call", { status: 400 });
  }

  // +++
  // Using `@liveblocks/emails` to create an email
  if (isThreadNotificationEvent(event)) {
    const emailData = await prepareThreadNotificationEmailAsReact(
      liveblocks,
      event
    );

    if (emailData.type === "unreadMention") {
      const email = (
        <div>
          <div>
            @{emailData.comment.author.id} at {emailData.comment.createdAt}
          </div>
          <div>{emailData.comment.body}</div>
        </div>
      );

      // Send unread mention email
      // ...
    }
  }
  // +++

  return new Response(null, { status: 200 });
}
```

**We’ll only show the highlighted part below**, as it’s assumed you’ve set this
already, and your file contains `liveblocks` and `event`.

### End-to-end guides

We have two guides that take you through every step of setting up your email
notifications, including setting up webhooks:

- [How to send email notifications of unread comments](/docs/guides/how-to-send-email-notifications-of-unread-comments).
- [How to send email notifications for unread text editor mentions ](/docs/guides/how-to-send-email-notifications-for-unread-text-editor-mentions).

### Ready-made email templates

We have a number of examples that show you how to set up emails with your
Comments or Text Editor application. Each [Resend](https://resend.com) example
has full ready-made email templates inside, which are a great starting point for
your application.

- [Comments + Resend](/examples/comments-emails/nextjs-comments-emails-resend).
- [Comments + SendGrid](/examples/comments-emails/nextjs-comments-emails-sendgrid).
- [Text Editor/Tiptap + Resend](/examples/collaborative-text-editor-emails/nextjs-tiptap-emails-resend).
- [Text Editor/Lexical + Resend](/examples/collaborative-text-editor-emails/nextjs-lexical-emails-resend).

## Thread notification emails [#thread-notification-emails]

These functions help you create emails to notify users of _unread comments_ in
threads. They fetch each relevant comment, filtering out any that have already
been read, and help you style each comment’s body with either
[React](#prepare-thread-notification-email-as-react) or
[HTML](#prepare-thread-notification-email-as-html).

<Figure>
  <Image
    src="/assets/emails/new-comments-email.png"
    alt="An email showing 7 new comments, with comment bodies and links to each comment"
    width={1567}
    height={830}
  />
</Figure>

<Banner>

This screenshot shows a ready-made template from our
[Comments + Resend](/examples/comments-emails/nextjs-comments-emails-resend)
example.

</Banner>

These functions also help you distinguish between _unread mentions_ and _unread
replies_.

A thread has _unread replies_ if a comment was created after the `readAt` date
on the notification, and created before or at the same time as the `notifiedAt`
date. All unread replies are returned in an array.

```js
{
  type: "unreadReplies",
  roomId: "my-room-id",
  comments: [
    {/* Comment data */},
    // ...
  ],
}
```

A thread has an _unread mention_ if it has unread replies, and one of the
replies mentions the user. A single comment with the latest mention is returned.

```js
{
  type: "unreadMention",
  roomId: "my-room-id",
  comment: {/* Comment data */},
}
```

### prepareThreadNotificationEmailAsReact [#prepare-thread-notification-email-as-react]

Takes a
[thread notification webhook event](/docs/platform/webhooks#Thread-notification)
and returns unread comment body(s) related to the notification, as React nodes.
It can return one of three formats, an `unreadMention` type containing one
comment, an `unreadReplies` type returning multiple comments, or `null` if there
are no unread mentions/replies. You can also
[resolve public data](#prepare-thread-notification-email-as-react-resolving-data)
and
[customize the components](#prepare-thread-notification-email-as-react-customizing-components).

```tsx
import { prepareThreadNotificationEmailAsReact } from "@liveblocks/emails";
import { isThreadNotificationEvent } from "@liveblocks/node";

// Get `liveblocks` and `event` (see "Setup" section)
// ...

if (isThreadNotificationEvent(event)) {
  // +++
  const emailData = await prepareThreadNotificationEmailAsReact(
    liveblocks,
    event
  );
  // +++
  let email;

  switch (emailData.type) {
    case "unreadMention": {
      email = (
        <div>
          <div>
            @{emailData.comment.author.id} at {emailData.comment.createdAt}
          </div>
          <div>{emailData.comment.body}</div>
        </div>
      );
      break;
    }

    case "unreadReplies": {
      email = (
        <div>
          {emailData.comments.map((comment) => (
            <div key={comment.id}>
              <div>
                @{comment.author.id} at {comment.createdAt}
              </div>
              <div>{comment.body}</div>
            </div>
          ))}
        </div>
      );
      break;
    }
  }
}

// Send your email
// ...
```

It’s designed to be used in a webhook event, which requires a
[`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client) Node.js
client and a
[`WebhookHandler`](/docs/api-reference/liveblocks-node#WebhookHandler). Check
for the correct webhook event using
[`isThreadNotificationEvent`](/docs/api-reference/liveblocks-node#isThreadNotificationEvent)
before running the function, such as in this Next.js route handler.

```tsx title="Full Next.js route handler example" isCollapsed isCollapsable
import { isThreadNotificationEvent, WebhookHandler } from "@liveblocks/node";
import { Liveblocks } from "@liveblocks/node";
// +++
import { prepareThreadNotificationEmailAsReact } from "@liveblocks/emails";
// +++

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

const webhookHandler = new WebhookHandler(
  process.env.LIVEBLOCKS_WEBHOOK_SECRET_KEY as string
);

export async function POST(request: Request) {
  const body = await request.json();
  const headers = request.headers;

  // Verify if this is a real webhook request
  let event;
  try {
    event = webhookHandler.verifyRequest({
      headers: headers,
      rawBody: JSON.stringify(body),
    });
  } catch (err) {
    console.error(err);
    return new Response("Could not verify webhook call", { status: 400 });
  }

  // +++
  if (isThreadNotificationEvent(event)) {
    const emailData = await prepareThreadNotificationEmailAsReact(
      liveblocks,
      event
    );
    let email;

    switch (emailData.type) {
      case "unreadMention": {
        email = (
          <div>
            <div>
              @{emailData.comment.author.id} at {emailData.comment.createdAt}
            </div>
            <div>{emailData.comment.body}</div>
          </div>
        );
        break;
      }

      case "unreadReplies": {
        email = (
          <div>
            {emailData.comments.map((comment) => (
              <div key={comment.id}>
                <div>
                  @{comment.author.id} at {comment.createdAt}
                </div>
                <div>{comment.body}</div>
              </div>
            ))}
          </div>
        );
        break;
      }
    }

    // Send your email
    // ...
  }
  // +++

  return new Response(null, { status: 200 });
}
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="value"
    type="ThreadNotificationEmailDataAsReact | null"
  >
    Returns comment information, and a formatted React body, ready for use in emails. Returns `null` if there are no unread mentions or replies. The result has two formats depending on whether this notification is for a *single unread mention*, or for *multiple unread replies*:

    ```js title="Unread mention" isCollapsable isCollapsed
    {
      type: "unreadMention",
      roomId: "my-room-id",

      // An unread mention has just one comment
      comment: {
        id: "cm_asfs8f...",
        threadId: "th_sj30as...",
        createdAt: Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>,

        // The formatted comment, pass it to React `children`
        body: { /* ... */},

        author: {
          id: "aurélien@example.com",
          info: { /* Custom user info you have resolved */ },
        },
      },
    }
    ```

    <div className="-mt-2">

    ```js title="Unread replies" isCollapsable isCollapsed
    {
      type: "unreadReplies",
      roomId: "my-room-id",

      // Unread replies means multiple comments
      comments: [
        {
          id: "cm_asfs8f...",
          threadId: "th_sj30as..."
          createdAt: Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>,

          // The formatted comment, pass it to React `children`
          body: { /* ... */},

          author: {
            id: "aurélien@example.com",
            info: { /* Custom user info you have resolved */ },
          },
        },

        // More comments
        //...
      ],
    }
    ```

    </div>

  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="client" type="Liveblocks" required>
    A [`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client)
    Node.js client.
  </PropertiesListItem>
  <PropertiesListItem name="event" type="ThreadNotificationEvent" required>
    An object passed from a webhook event, specifically the
    [`ThreadNotificationEvent`](/docs/platform/webhooks#Thread-notification).
    [Learn more about setting this up](#Setup).
  </PropertiesListItem>
  <PropertiesListItem name="options" type="object">
    A number of options to customize the format of the comments, adding user
    info, room info, and styles.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveUsers"
    detailedType='async? (args: ResolveUsersArgs) => (UserMeta["info"] | undefined)[] | undefined'
  >
    A function that resolves user information. Return an array of
    `UserMeta["info"]` objects in the same order they arrived. Works similarly
    to the [resolver on the
    client](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers).
    [Learn more](#prepare-thread-notification-email-as-react-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveGroupsInfo"
    detailedType="async? (args: ResolveGroupsInfoArgs) => (GroupInfo | undefined)[] | undefined"
  >
    A function that resolves group information. Return an array of `GroupInfo`
    objects in the same order they arrived. Works similarly to the [resolver on
    the
    client](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo).
    [Learn more](#prepare-thread-notification-email-as-react-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveRoomInfo"
    detailedType="async? (args: ResolveRoomInfoArgs) => RoomInfo | undefined"
  >
    A function that resolves room information. Return a `RoomInfo` object, as
    matching your types. Works similarly to the [resolver on the
    client](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveRoomsInfo)
    but for one room. [Learn
    more](#prepare-thread-notification-email-as-react-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem name="options.components" type="object">
    Pass different React components to customize the elements in the comment
    bodies. Five components can be passed to the object: `Container`,
    `Paragraph`, `Text`, `Link`, `Mention`. [Learn
    more](#prepare-thread-notification-email-as-react-customizing-components).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.components.Container"
    type="({ children: ReactNode }) => ReactNode"
  >
    The comment body container.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.components.Paragraph"
    type="({ children: ReactNode }) => ReactNode"
  >
    The paragraph block.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.components.Text"
    type="({ children: ReactNode }) => ReactNode"
  >
    The text element.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.components.Link"
    detailedType="({ element: CommentBodyLink, href: string }) => ReactNode"
  >
    The link element.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.components.Mention"
    detailedType={`({ element: CommentBodyMention, user?: UserInfo["info"], group?: GroupInfo }) => ReactNode`}
  >
    The mention element.
  </PropertiesListItem>
</PropertiesList>

#### Resolving data [#prepare-thread-notification-email-as-react-resolving-data]

Similarly to on the client, you can resolve
[users](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers),
[group info](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo),
and
[room info](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveRoomsInfo),
making it easier to render your emails. For example, you can resolve a user’s ID
into their name, and show their name in the email.

<Banner title="Resolving users and groups" type="info">
  When resolving users and groups, the function receives a list of IDs and you
  should return a list of objects of the same size, in the same order.
</Banner>

```tsx
const emailData = await prepareThreadNotificationEmailAsReact(
  liveblocks,
  webhookEvent,
  {
    // +++
    resolveUsers: async ({ userIds }) => {
      const usersData = await __getUsersFromDB__(userIds);

      return usersData.map((userData) => ({
        name: userData.name, // "Nimesh"
        avatar: userData.avatar.src, // "https://..."
      }));
    },
    resolveGroupsInfo: async ({ groupIds }) => {
      const groupsData = await __getGroupsFromDB__(groupIds);

      return groupsData.map((groupData) => ({
        name: groupData.name, // "Engineering"
        avatar: groupData.avatar.src, // "https://..."
      }));
    },
    resolveRoomInfo({ roomId }) {
      return {
        name: roomId, // "my-room-name"
        url: `https://example.com/${roomId}`,
      };
    },
    // +++
  }
);

// { type: "unreadMention", comment: { ... }, ... }
console.log(emailData);

// { name: "Nimesh", avatar: "https://..." }
console.log(emailData.comment.author.info);

// { name: "my-room-name", url: "https://example.com/my-room-name" }
console.log(emailData.roomInfo);
```

#### Customizing components [#prepare-thread-notification-email-as-react-customizing-components]

Each React component in the comment body can be replaced with a custom React
component, if you wish to apply different styles. Five components are available:
`Container`, `Paragraph`, `Text`, `Link`, `Mention`.

```tsx
const emailData = await prepareThreadNotificationEmailAsReact(
  liveblocks,
  webhookEvent,
  {
    // +++
    components: {
      Paragraph: ({ children }) => <p>{children}</p>,

      // `react-email` components are supported
      Text: ({ children }) => (
        <Text className="text-sm text-black m-0 mb-4">{children}</Text>
      ),

      // `user` and `group` are the optional data returned from `resolveUsers` and `resolveGroupsInfo`
      Mention: ({ element, user, group }) => (
        <span style={{ color: "red" }}>
          @{user?.name ?? group?.name ?? element.id}
        </span>
      ),

      // If the link is rich-text render it, otherwise use the URL
      Link: ({ element, href }) => <a href={href}>{element?.text ?? href}</a>,
    },
    // +++
  }
);

// { type: "unreadMention", comment: { ... }, ... }
console.log(emailData);

// The previously defined components are used in the body property, now formatted as React nodes.
console.log(emailData.comment.body);
```

### prepareThreadNotificationEmailAsHtml [#prepare-thread-notification-email-as-html]

Takes a
[thread notification webhook event](/docs/platform/webhooks#Thread-notification)
and returns unread comment body(s) related to the notification, as an HTML-safe
string. It can return one of three formats, an `unreadMention` type containing
one comment, an `unreadReplies` type returning multiple comments, or `null` if
there are no unread mentions/replies. You can also
[resolve public data](#prepare-thread-notification-email-as-html-resolving-data)
and
[customize the styles](#prepare-thread-notification-email-as-html-styling-elements).

```ts
import { prepareThreadNotificationEmailAsHtml } from "@liveblocks/emails";
import { isThreadNotificationEvent } from "@liveblocks/node";

// Get `liveblocks` and `event` (see "Setup" section)
// ...

if (isThreadNotificationEvent(event)) {
  // +++
  const emailData = await prepareThreadNotificationEmailAsHtml(
    liveblocks,
    event
  );
  // +++
  let email;

  switch (emailData.type) {
    case "unreadMention": {
      email = `
        <div>
          <div>
            @${emailData.comment.author.id} at ${emailData.comment.createdAt}
          </div>
          <div>${emailData.comment.body}</div>
        </div>
      `;
      break;
    }

    case "unreadReplies": {
      email = `
        <div>
          ${emailData.comments
            .map(
              (comment) => `
                <div>
                  <div>
                    @${comment.author.id} at ${comment.createdAt}
                  </div>
                  <div>${comment.body}</div>
                </div>
              `
            )
            .join("")}
        </div>
      `;
      break;
    }
  }
}

// Send your email
// ...
```

It’s designed to be used in a webhook event, which requires a
[`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client) Node.js
client, a
[`WebhookHandler`](/docs/api-reference/liveblocks-node#WebhookHandler). Check
for the correct webhook event using
[`isThreadNotificationEvent`](/docs/api-reference/liveblocks-node#isThreadNotificationEvent)
before running the function, such as in this Next.js route handler.

```tsx title="Full Next.js route handler example" isCollapsed isCollapsable
import { isThreadNotificationEvent, WebhookHandler } from "@liveblocks/node";
import { Liveblocks } from "@liveblocks/node";
// +++
import { prepareThreadNotificationEmailAsHtml } from "@liveblocks/emails";
// +++

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

const webhookHandler = new WebhookHandler(
  process.env.LIVEBLOCKS_WEBHOOK_SECRET_KEY as string
);

export async function POST(request: Request) {
  const body = await request.json();
  const headers = request.headers;

  // Verify if this is a real webhook request
  let event;
  try {
    event = webhookHandler.verifyRequest({
      headers: headers,
      rawBody: JSON.stringify(body),
    });
  } catch (err) {
    console.error(err);
    return new Response("Could not verify webhook call", { status: 400 });
  }

  // +++
  if (isThreadNotificationEvent(event)) {
    const emailData = await prepareThreadNotificationEmailAsHtml(
      liveblocks,
      event
    );
    let email;

    switch (emailData.type) {
      case "unreadMention": {
        email = `
        <div>
          <div>
            @${emailData.comment.author.id} at ${emailData.comment.createdAt}
          </div>
          <div>${emailData.comment.body}</div>
        </div>
      `;
        break;
      }

      case "unreadReplies": {
        email = `
        <div>
          ${emailData.comments
            .map(
              (comment) => `
                <div>
                  <div>
                    @${comment.author.id} at ${comment.createdAt}
                  </div>
                  <div>${comment.body}</div>
                </div>
              `
            )
            .join("")}
        </div>
      `;
        break;
      }
    }

    // Send your email
    // ...
  }
  // +++

  return new Response(null, { status: 200 });
}
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="value"
    type="ThreadNotificationEmailDataAsHtml | null"
  >
    Returns comment information, and a formatted HTML body, ready for use in emails. Returns `null` if there are no unread mentions or comments. The result has two formats depending on whether this notification is for a *single unread mention*, or for *multiple unread replies*:

    ```js title="Unread mention" isCollapsable isCollapsed
    {
      type: "unreadMention",
      roomId: "my-room-id",

      // An unread mention has just one comment
      comment: {
        id: "cm_asfs8f...",
        threadId: "th_sj30as...",
        createdAt: Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>,

        // The formatted comment, as an HTML string
        body: "<div>...</div>",

        author: {
          id: "aurélien@example.com",
          info: { /* Custom user info you have resolved */ },
        },
      },
    }
    ```

    <div className="-mt-2">

      ```js title="Unread replies" isCollapsable isCollapsed
      {
        type: "unreadReplies",
        roomId: "my-room-id",

        // Unread replies means multiple comments
        comments: [
          {
            id: "cm_asfs8f...",
            threadId: "th_sj30as...",
            createdAt: Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>,

            // The formatted comment, as an HTML string
            body: "<div>...</div>",

            author: {
              id: "aurélien@example.com",
              info: { /* Custom user info you have resolved */ },
            },
          },

        // More comments
        //...
        ],
      }
      ```

    </div>

  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="client" type="Liveblocks" required>
    A [`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client)
    Node.js client.
  </PropertiesListItem>
  <PropertiesListItem name="event" type="ThreadNotificationEvent" required>
    An object passed from a webhook event, specifically the
    [`ThreadNotificationEvent`](/docs/platform/webhooks#Thread-notification).
    [Learn more about setting this up](#Setup).
  </PropertiesListItem>
  <PropertiesListItem name="options" type="object">
    A number of options to customize the format of the comments, adding user
    info, room info, and styles.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveUsers"
    detailedType='async? (args: ResolveUsersArgs) => (UserMeta["info"] | undefined)[] | undefined'
  >
    A function that resolves user information in
    [Comments](/docs/ready-made-features/comments). Return an array of
    `UserMeta["info"]` objects in the same order they arrived. Works similarly
    to the [resolver on the
    client](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers).
    [Learn more](#prepare-thread-notification-email-as-html-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveGroupsInfo"
    detailedType="async? (args: ResolveGroupsInfoArgs) => (GroupInfo | undefined)[] | undefined"
  >
    A function that resolves group information. Return an array of `GroupInfo`
    objects in the same order they arrived. Works similarly to the [resolver on
    the
    client](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo).
    [Learn more](#prepare-thread-notification-email-as-react-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveRoomInfo"
    detailedType="async? (args: ResolveRoomInfoArgs) => RoomInfo | undefined"
  >
    A function that resolves room information. Return a `RoomInfo` object, as
    matching your types. Works similarly to the [resolver on the
    client](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveRoomsInfo)
    but for one room. [Learn
    more](#prepare-thread-notification-email-as-html-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem name="options.styles" type="object">
    Pass CSS properties to style the different HTML elements in the comment
    bodies. Five elements can be styled: `paragraph`, `code`, `strong`, `link`,
    `mention`. [Learn
    more](#prepare-thread-notification-email-as-html-styling-elements).
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.container" type="CSSProperties">
    Inline styles to apply to the comment body container.
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.paragraph" type="CSSProperties">
    Inline styles to apply to the paragraph block.
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.code" type="CSSProperties">
    Inline styles to apply to the code element.
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.strong" type="CSSProperties">
    Inline styles to apply to the strong element.
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.mention" type="CSSProperties">
    Inline styles to apply to the mention element.
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.link" type="CSSProperties">
    Inline styles to apply to the link element.
  </PropertiesListItem>
</PropertiesList>

#### Resolving data [#prepare-thread-notification-email-as-html-resolving-data]

Similarly to on the client, you can resolve
[users](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers),
[group info](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo),
and
[room info](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveRoomsInfo),
making it easier to render your emails. For example, you can resolve a user’s ID
into their name, and show their name in the email.

<Banner title="Resolving users and groups" type="info">
  When resolving users and groups, the function receives a list of IDs and you
  should return a list of objects of the same size, in the same order.
</Banner>

```tsx
const emailData = await prepareThreadNotificationEmailAsHtml(
  liveblocks,
  webhookEvent,
  {
    // +++
    resolveUsers: async ({ userIds }) => {
      const usersData = await __getUsersFromDB__(userIds);

      return usersData.map((userData) => ({
        name: userData.name, // "Nimesh"
        avatar: userData.avatar.src, // "https://..."
      }));
    },
    resolveGroupsInfo: async ({ groupIds }) => {
      const groupsData = await __getGroupsFromDB__(groupIds);

      return groupsData.map((groupData) => ({
        name: groupData.name, // "Engineering"
        avatar: groupData.avatar.src, // "https://..."
      }));
    },
    resolveRoomInfo({ roomId }) {
      return {
        name: roomId, // "my-room-name"
        url: `https://example.com/${roomId}`,
      };
    },
    // +++
  }
);

// { type: "unreadMention", comment: { ... }, ... }
console.log(emailData);

// { name: "Nimesh", avatar: "https://..." }
console.log(emailData.comment.author.info);

// { name: "my-room-name", url: "https://example.com/my-room-name" }
console.log(emailData.roomInfo);
```

#### Styling elements [#prepare-thread-notification-email-as-html-styling-elements]

Each element in the comment body can be styled with custom CSS properties, if
you would like to change the appearance. Five elements are available:
`paragraph`, `code`, `strong`, `mention`, `link`.

```tsx
const emailData = await prepareThreadNotificationEmailAsHtml(
  liveblocks,
  webhookEvent,
  {
    // +++
    styles: {
      paragraph: { margin: "12px 0" },

      mention: {
        fontWeight: "bold",
        color: "red",
      },

      link: {
        textDecoration: "underline",
      },
    },
    // +++
  }
);

// { type: "unreadMention", comment: { ... }, ... }
console.log(emailData);

// The elements in the comment body are now styled
console.log(emailData.comment.body);
```

## Text Mention notification emails [#text-mention-notification-emails]

These functions help you create emails to notify users when they have an _unread
mention_ in a [Text Editor](/docs/ready-made-features/text-editor) document. In
this case, a mention is not related to comments, but is instead an inline
mention inside the text editor itself. If the mention has not been read, the
functions fetch a text mention and its surrounding text, giving you more
context, and helping you style the mention content with either
[React](#prepare-text-mention-notification-email-as-react) or
[HTML](#prepare-text-mention-notification-email-as-html).

<Figure>
  <Image
    src="/assets/emails/new-text-mention-email.png"
    alt="An email showing a text mention in a text editor document"
    width={1126}
    height={470}
  />
</Figure>

<Banner>

This screenshot shows a ready-made template from our
[Text Editor + Resend](/examples/collaborative-text-editor-emails/nextjs-tiptap-emails-resend)
examples.

</Banner>

The functions help to determine if the mention still exists in the document and
will indicate that there’s no email to send in this case. Currently, only
mentions in paragraph blocks create notifications, as there are limitations
around retrieving mentions in plugins.

### Limitations

Before you get started, there are some limitations with text mentions that you
should be aware of.

#### Mentions in plugins

If a user is mentioned in a plugin or extension, a text mention notification is
not sent. This is because Liveblocks doesn’t know the exact schema of your
editor and all its plugins, and we can’t extract the data correctly. This means
that _only mentions in paragraph blocks are sent_, and mentions in lists,
checkboxes, etc., are not, as they are all powered by plugins. We’re
investigating solutions for this, and we’d like to
[hear from you](/contact/support) if you have any thoughts.

#### Multiple Tiptap editors

Tiptap optionally allows you to
[render multiple editors per page](/docs/ready-made-features/text-editor/tiptap#Multiple-editors),
instead of just one. For now, these functions only support one editor per room,
but we’ll be looking to add support for more later.

#### BlockNote

This package does not yet support our
[collaborative BlockNote text editor](/docs/api-reference/liveblocks-react-blocknote)
integration. Support of BlockNote is planned for a future release and is
currently on our development roadmap. Users requiring BlockNote compatibility
should monitor package updates for this upcoming feature.

### prepareTextMentionNotificationEmailAsReact [#prepare-text-mention-notification-email-as-react]

Takes a
[text mention notification webhook event](/docs/platform/webhooks#TextMention-notification)
and returns an unread text mention with its surrounding text as React nodes. It
can also return `null` if the text mention does not exist anymore or has been
already been read. You can also
[resolve public data](#prepare-text-mention-notification-email-as-react-resolving-data)
and
[customize the components](#prepare-text-mention-notification-email-as-react-customizing-components).

```tsx
import { prepareTextMentionNotificationEmailAsReact } from "@liveblocks/emails";
import { isTextMentionNotificationEvent } from "@liveblocks/node";

// Get `liveblocks` and `event` (see "Setup" section)
// ...

if (isTextMentionNotificationEvent(event)) {
  // +++
  const emailData = await prepareTextMentionNotificationEmailAsReact(
    liveblocks,
    event
  );
  // +++

  const email = (
    <div>
      <div>
        @{emailData.mention.author.id} at {emailData.mention.createdAt}
      </div>
      <div>{emailData.mention.content}</div>
    </div>
  );
}

// Send your email
// ...
```

It’s designed to be used in a webhook event, which requires a
[`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client) Node.js
client and a
[`WebhookHandler`](/docs/api-reference/liveblocks-node#WebhookHandler). Check
for the correct webhook event using
[`isTextMentionNotificationEvent`](/docs/api-reference/liveblocks-node#isTextMentionNotificationEvent)
before running the function, such as in this Next.js route handler.

```tsx title="Full Next.js route handler example" isCollapsed isCollapsable
import {
  isTextMentionNotificationEvent,
  WebhookHandler,
} from "@liveblocks/node";
import { Liveblocks } from "@liveblocks/node";
// +++
import { prepareTextMentionNotificationEmailAsReact } from "@liveblocks/emails";
// +++

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

const webhookHandler = new WebhookHandler(
  process.env.LIVEBLOCKS_WEBHOOK_SECRET_KEY as string
);

export async function POST(request: Request) {
  const body = await request.json();
  const headers = request.headers;

  // Verify if this is a real webhook request
  let event;
  try {
    event = webhookHandler.verifyRequest({
      headers: headers,
      rawBody: JSON.stringify(body),
    });
  } catch (err) {
    console.error(err);
    return new Response("Could not verify webhook call", { status: 400 });
  }

  // +++
  if (isTextMentionNotificationEvent(event)) {
    const emailData = await prepareTextMentionNotificationEmailAsReact(
      liveblocks,
      event
    );

    const email = (
      <div>
        <div>
          @{emailData.mention.author.id} at {emailData.mention.createdAt}
        </div>
        <div>{emailData.mention.content}</div>
      </div>
    );

    // Send your email
    // ...
  }
  // +++

  return new Response(null, { status: 200 });
}
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="value"
    type="TextMentionNotificationEmailDataAsReact | null"
  >
    Returns text mention information, and a formatted React content ready for
    use in emails. Returns `null` if the text mention does not exist anymore or
    has already been read.

    ```js title="Unread text mention"
    {
      roomInfo: {
        name: "my room name",
        url: "https://my-room-url.io"
      },
      mention: {
        kind: "user",
        textMentionId: "in_oiujhdg...",
        id: "user-0",
        roomId: "my-room-id",
        createdAt: Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>,

        // The formatted content, pass it to React `children`
        content: { /* ... */}

        author: {
          id: "vincent@example.com",
          info: { /* Custom user info you have resolved */ }
        }
      },
    }
    ```

  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="client" type="Liveblocks" required>
    A [`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client)
    Node.js client.
  </PropertiesListItem>
  <PropertiesListItem name="event" type="TextMentionNotificationEvent" required>
    An object passed from a webhook event, specifically the
    [`TextMentionNotificationEvent`](/docs/platform/webhooks#TextMention-notification).
    [Learn more about setting this up](#Setup).
  </PropertiesListItem>
  <PropertiesListItem name="options" type="object">
    A number of options to customize the format of the content, adding user
    info, room info, and styles.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveUsers"
    detailedType='async? (args: ResolveUsersArgs) => (UserMeta["info"] | undefined)[] | undefined'
  >
    A function that resolves user information in
    [Comments](/docs/ready-made-features/comments). Return an array of
    `UserMeta["info"]` objects in the same order they arrived. Works similarly
    to the [resolver on the
    client](/docs/ready-made-features/text-editor/lexical#Users-and-mentions).
    [Learn
    more](#prepare-text-mention-notification-email-as-react-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveGroupsInfo"
    detailedType="async? (args: ResolveGroupsInfoArgs) => (GroupInfo | undefined)[] | undefined"
  >
    A function that resolves group information. Return an array of `GroupInfo`
    objects in the same order they arrived. Works similarly to the [resolver on
    the
    client](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo).
    [Learn more](#prepare-thread-notification-email-as-react-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveRoomInfo"
    detailedType="async? (args: ResolveRoomInfoArgs) => RoomInfo | undefined"
  >
    A function that resolves room information. Return a `RoomInfo` object, as
    matching your types. Works similarly to the [resolver on the
    client](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveRoomsInfo)
    but for one room. [Learn
    more](#prepare-text-mention-notification-email-as-react-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem name="options.components" type="object">
    Pass different React components to customize the elements in the mention
    content. Three components can be passed to the object: `Container`, `Text`,
    and `Mention`. [Learn
    more](#prepare-text-mention-notification-email-as-react-customizing-components).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.components.Container"
    type="({ children: ReactNode }) => ReactNode"
  >
    The mention and its surrounding text container
  </PropertiesListItem>
  <PropertiesListItem
    name="options.components.Text"
    type="({ children: ReactNode }) => ReactNode"
  >
    The text element.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.components.Mention"
    detailedType={`({ element: LiveblocksTextEditorMentionNode, user?: UserInfo["info"], group?: GroupInfo }) => ReactNode`}
  >
    The mention element.
  </PropertiesListItem>
</PropertiesList>

#### Resolving data [#prepare-text-mention-notification-email-as-react-resolving-data]

Similarly to on the client, you can resolve
[users](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers),
[group info](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo),
and
[room info](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveRoomsInfo),
making it easier to render your emails. For example, you can resolve a user’s ID
into their name, and show their name in the email.

<Banner title="Resolving users and groups" type="info">
  When resolving users and groups, the function receives a list of IDs and you
  should return a list of objects of the same size, in the same order.
</Banner>

```tsx
const emailData = await prepareTextMentionNotificationEmailAsReact(
  liveblocks,
  webhookEvent,
  {
    // +++
    resolveUsers: async ({ userIds }) => {
      const usersData = await __getUsersFromDB__(userIds);

      return usersData.map((userData) => ({
        name: userData.name, // "Nimesh"
        avatar: userData.avatar.src, // "https://..."
      }));
    },
    resolveGroupsInfo: async ({ groupIds }) => {
      const groupsData = await __getGroupsFromDB__(groupIds);

      return groupsData.map((groupData) => ({
        name: groupData.name, // "Engineering"
        avatar: groupData.avatar.src, // "https://..."
      }));
    },
    resolveRoomInfo({ roomId }) {
      return {
        name: roomId, // "my-room-name"
        url: `https://example.com/${roomId}`,
      };
    },
    // +++
  }
);

// { mention: { ... }, ... }
console.log(emailData);

// { name: "Nimesh", avatar: "https://..." }
console.log(emailData.mention.author.info);

// { name: "my-room-name", url: "https://example.com/my-room-name" }
console.log(emailData.roomInfo);
```

#### Customizing components [#prepare-text-mention-notification-email-as-react-customizing-components]

Each React component in the mention context can be replaced with a custom React
component, if you wish to apply different styles. Three components are
available: `Container`, `Text`, and `Mention`.

```tsx
const emailData = await prepareTextMentionNotificationEmailAsReact(
  liveblocks,
  webhookEvent,
  {
    // +++
    components: {
      // `react-email` components are supported
      Container: ({ children }) => <Section>{children}</Section>,

      Text: ({ children }) => (
        <Text className="text-sm text-black m-0 mb-4">{children}</Text>
      ),

      // `user` and `group` are the optional data returned from `resolveUsers` and `resolveGroupsInfo`
      Mention: ({ element, user, group }) => (
        <span style={{ color: "red" }}>
          @{user?.name ?? group?.name ?? element.id}
        </span>
      ),
    },
    // +++
  }
);

// { mention: { ... }, ... }
console.log(emailData);

// The components are now used in this React content
console.log(emailData.mention.content);
```

### prepareTextMentionNotificationEmailAsHtml [#prepare-text-mention-notification-email-as-html]

Takes a
[text mention notification webhook event](/docs/platform/webhooks#TextMention-notification)
and returns an unread text mention with its surrounding text as an HTML string.
It can also return `null` if the text mention does not exist anymore or has been
already been read. You can also
[resolve public data](#prepare-text-mention-notification-email-as-html-resolving-data)
and
[customize the styles](#prepare-text-mention-notification-email-as-html-styling-elements).

```tsx
import { prepareTextMentionNotificationEmailAsHtml } from "@liveblocks/emails";
import { isTextMentionNotificationEvent } from "@liveblocks/node";

// Get `liveblocks` and `event` (see "Setup" section)
// ...

if (isTextMentionNotificationEvent(event)) {
  // +++
  const emailData = await prepareTextMentionNotificationEmailAsHtml(
    liveblocks,
    event
  );
  // +++

  const email = (
    <div>
      <div>
        @{emailData.mention.author.id} at {emailData.mention.createdAt}
      </div>
      <div>{emailData.mention.content}</div>
    </div>
  );
}

// Send your email
// ...
```

It’s designed to be used in a webhook event, which requires a
[`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client) Node.js
client and a
[`WebhookHandler`](/docs/api-reference/liveblocks-node#WebhookHandler). Check
for the correct webhook event using
[`isTextMentionNotificationEvent`](/docs/api-reference/liveblocks-node#isTextMentionNotificationEvent)
before running the function, such as in this Next.js route handler.

```tsx title="Full Next.js route handler example" isCollapsed isCollapsable
import {
  isTextMentionNotificationEvent,
  WebhookHandler,
} from "@liveblocks/node";
import { Liveblocks } from "@liveblocks/node";
// +++
import { prepareTextMentionNotificationEmailAsHtml } from "@liveblocks/emails";
// +++

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

const webhookHandler = new WebhookHandler(
  process.env.LIVEBLOCKS_WEBHOOK_SECRET_KEY as string
);

export async function POST(request: Request) {
  const body = await request.json();
  const headers = request.headers;

  // Verify if this is a real webhook request
  let event;
  try {
    event = webhookHandler.verifyRequest({
      headers: headers,
      rawBody: JSON.stringify(body),
    });
  } catch (err) {
    console.error(err);
    return new Response("Could not verify webhook call", { status: 400 });
  }

  // +++
  if (isTextMentionNotificationEvent(event)) {
    const emailData = await prepareTextMentionNotificationEmailAsHtml(
      liveblocks,
      event
    );

    const email = `
    <div>
      <div>
        @${emailData.mention.author.id} at ${emailData.mention.createdAt}
      </div>
      <div>${emailData.mention.content}</div>
    </div>
    `;

    // Send your email
    // ...
  }
  // +++

  return new Response(null, { status: 200 });
}
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="value"
    type="TextMentionNotificationEmailDataAsHtml | null"
  >
    Returns text mention information, and formatted HTML content ready for
    use in emails. Returns `null` if the text mention does not exist anymore or
    has already been read.

    ```js title="Unread text mention"
    {
      roomInfo: {
        name: "my room name"
        url: "https://my-room-url.io"
      },
      mention: {
        kind: "user",
        textMentionId: "in_oiujhdg...",
        id: "user-0",
        roomId: "my-room-id",
        createdAt: Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>,

        // The formatted content, as an HTML string
        content: { /* ... */}

        author: {
          id: "vincent@example.com",
          info: { /* Custom user info you have resolved */ }
        }
      },
    }
    ```

  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="client" type="Liveblocks" required>
    A [`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client)
    Node.js client.
  </PropertiesListItem>
  <PropertiesListItem name="event" type="TextMentionNotificationEvent" required>
    An object passed from a webhook event, specifically the
    [`TextMentionNotificationEvent`](/docs/platform/webhooks#TextMention-notification).
    [Learn more about setting this up](#Setup).
  </PropertiesListItem>
  <PropertiesListItem name="options" type="object">
    A number of options to customize the format of the content, adding user
    info, room info, and styles.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveUsers"
    detailedType='async? (args: ResolveUsersArgs) => (UserMeta["info"] | undefined)[] | undefined'
  >
    A function that resolves user information in
    [Comments](/docs/ready-made-features/comments). Return an array of
    `UserMeta["info"]` objects in the same order they arrived. Works similarly
    to the [resolver on the
    client](/docs/ready-made-features/text-editor/lexical#Users-and-mentions).
    [Learn
    more](#prepare-text-mention-notification-email-as-html-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveGroupsInfo"
    detailedType="async? (args: ResolveGroupsInfoArgs) => (GroupInfo | undefined)[] | undefined"
  >
    A function that resolves group information. Return an array of `GroupInfo`
    objects in the same order they arrived. Works similarly to the [resolver on
    the
    client](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo).
    [Learn more](#prepare-thread-notification-email-as-react-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.resolveRoomInfo"
    detailedType="async? (args: ResolveRoomInfoArgs) => RoomInfo | undefined"
  >
    A function that resolves room information. Return a `RoomInfo` object, as
    matching your types. Works similarly to the [resolver on the
    client](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveRoomsInfo)
    but for one room. [Learn
    more](#prepare-text-mention-notification-email-as-html-resolving-data).
  </PropertiesListItem>
  <PropertiesListItem name="options.styles" type="object">
    Pass CSS properties to style the different HTML elements in the mention
    content. Four elements can be styled: `paragraph`, `code`, `strong`,
    `mention`, and, `link`. [Learn
    more](#prepare-text-mention-notification-email-as-html-styling-elements).
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.paragraph" type="CSSProperties">
    Inline styles to apply to the mention container. It's a `<div />` element
    under the hood.
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.code" type="CSSProperties">
    Inline styles to apply to the code element.
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.strong" type="CSSProperties">
    Inline styles to apply to the strong element.
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.mention" type="CSSProperties">
    Inline styles to apply to the mention element.
  </PropertiesListItem>
  <PropertiesListItem name="options.styles.link" type="CSSProperties">
    Inline styles to apply to the link element.
  </PropertiesListItem>
</PropertiesList>

#### Resolving data [#prepare-text-mention-notification-email-as-html-resolving-data]

Similarly to on the client, you can resolve
[users](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers),
[group info](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo),
and
[room info](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveRoomsInfo),
making it easier to render your emails. For example, you can resolve a user’s ID
into their name, and show their name in the email.

<Banner title="Resolving users and groups" type="info">
  When resolving users and groups, the function receives a list of IDs and you
  should return a list of objects of the same size, in the same order.
</Banner>

```tsx
const emailData = await prepareTextMentionNotificationEmailAsHtml(
  liveblocks,
  webhookEvent,
  {
    // +++
    resolveUsers: async ({ userIds }) => {
      const usersData = await __getUsersFromDB__(userIds);

      return usersData.map((userData) => ({
        name: userData.name, // "Nimesh"
        avatar: userData.avatar.src, // "https://..."
      }));
    },
    resolveGroupsInfo: async ({ groupIds }) => {
      const groupsData = await __getGroupsFromDB__(groupIds);

      return groupsData.map((groupData) => ({
        name: groupData.name, // "Engineering"
        avatar: groupData.avatar.src, // "https://..."
      }));
    },
    resolveRoomInfo({ roomId }) {
      return {
        name: roomId, // "my-room-name"
        url: `https://example.com/${roomId}`,
      };
    },
    // +++
  }
);

// { mention: { ... }, ... }
console.log(emailData);

// { name: "Nimesh", avatar: "https://..." }
console.log(emailData.mention.author.info);

// { name: "my-room-name", url: "https://example.com/my-room-name" }
console.log(emailData.roomInfo);
```

#### Styling elements [#prepare-text-mention-notification-email-as-html-styling-elements]

Each element in the comment body can be styled with custom CSS properties, if
you would like to change the appearance. Five elements are available:
`paragraph`, `code`, `strong`, `mention`, and `link`.

```tsx
const emailData = await prepareTextMentionNotificationEmailAsHtml(
  liveblocks,
  webhookEvent,
  {
    // +++
    styles: {
      paragraph: { margin: "12px 0" },

      mention: {
        fontWeight: "bold",
        color: "red",
      },
    },
    // +++
  }
);

// { mention: { ... }, ... }
console.log(emailData);

// The elements in the mention content are now styled
console.log(emailData.mention.content);
```

---
meta:
  title: "@liveblocks/node-lexical"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/node-lexical package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/node-lexical` provides a Node.js package to export and modify
[Lexical](https://lexical.dev/) documents on the server.

## withLexicalDocument

`withLexicalDocument` is the main entry point to modifying a document on the
server. It takes a room ID and a
[Liveblocks Node client](/docs/api-reference/liveblocks-node#Liveblocks-client),
and returns a callback used to work with Lexical documents stored in Liveblocks.

```ts highlight="8-14"
import { Liveblocks } from "@liveblocks/node";
import { withLexicalDocument } from "@liveblocks/node-lexical";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

await withLexicalDocument(
  { roomId: "your-room-id", client: liveblocks },
  async (doc) => {
    // Modify your Lexical `doc`
    // ...
  }
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="returns" type="T">
    Returns the value you return from the `doc` callback.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="roomId" type="string" required>
    The ID of the room to use.
  </PropertiesListItem>
  <PropertiesListItem name="client" type="Liveblocks" required>
    The [Liveblocks
    client](/docs/api-reference/liveblocks-node#Liveblocks-client) to use.
  </PropertiesListItem>
  <PropertiesListItem
    name="nodes"
    type="Klass<LexicalNode>[] | LexicalNodeReplacement[]"
  >
    Optional. The Lexical nodes used in the document. Will extend the default
    schema which uses Liveblocks mentions and Liveblocks comments.
  </PropertiesListItem>
</PropertiesList>

### Returning data

Get your editor’s text content by returning `doc.getTextContent` inside the
callback.

```ts
const textContent = await withLexicalDocument(
  { roomId: "my-room-id", client: liveblocks },
  // +++
  async (doc) => {
    return doc.getTextContent();
  }
  // +++
);

// "My content"
console.log(textContent);
```

### Custom nodes

If your Lexical document has custom nodes, they must be passed into the
`withLexicalDocument`, similarly to with a front end Lexical client.

```ts highlight="4"
import { CodeNode } from "@lexical/code";

await withLexicalDocument(
  { roomId: "my-room-id", client: liveblocks, nodes: [CodeNode] },
  async (doc) => {
    // Modify your Lexical `doc`
    // ...
  }
);
```

### Lexical document API

You can easily modify your document with the Lexical document API.

#### doc.update

Liveblocks provides `doc.update` which is a callback function similar to
Lexical’s `editor.update`. This makes it easy to use Lexical’s editor functions.
Any edits will be persisted and appear in realtime to connected users as soon as
the `update` promise resolves. Unlike Lexical’s `editor.update`, this change is
always discrete. The callback can also be an `async` function.

```ts
await withLexicalDocument(
  { roomId: "my-room-id", client: liveblocks },
  async (doc) => {
    // +++
    await doc.update(() => {
      // Make your modifications
      // ...
    });
    // +++
  }
);
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="callback" type="() => void">
    Callback function where you should handle your modifications.
  </PropertiesListItem>
</PropertiesList>

##### Example usage

Here’s an example of some modifications to a Lexical document.

```ts
import { $getRoot } from "lexical";
import { $createParagraphNode, $createTextNode } from "lexical/nodes";

await withLexicalDocument(
  { roomId: "my-room-id", client: liveblocks },
  async (doc) => {
    await doc.update(() => {
      // Adding a paragraph node with contained text node
      // +++
      const root = $getRoot();
      const paragraphNode = $createParagraphNode();
      const textNode = $createTextNode("Hello world");
      paragraphNode.append(textNode);
      root.append(paragraphNode);
      // +++
    });
  }
);
```

#### doc.getTextContent

Returns the text content from the root node as a `string`.

```ts
const textContent = await withLexicalDocument(
  { roomId: "my-room-id", client: liveblocks },
  // +++
  async (doc) => {
    return doc.getTextContent();
  }
  // +++
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="content" type="string">
    Returns the text retrieved from the document.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

#### doc.getEditorState

Returns Lexical’s [editorState](https://lexical.dev/docs/concepts/editor-state).

```ts
const editorState = await withLexicalDocument(
  { roomId: "my-room-id", client: liveblocks },
  // +++
  async (doc) => {
    return doc.getEditorState();
  }
  // +++
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="editorState" type="EditorState">
    Your editor’s Lexical state.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

#### doc.getLexicalEditor

Returns a headless Lexical editor.
[@lexical/headless](https://lexical.dev/docs/packages/lexical-headless).

```ts
const headlessEditor = await withLexicalDocument(
  { roomId: "my-room-id", client: liveblocks },
  // +++
  async (doc) => {
    return doc.getLexicalEditor();
  }
  // +++
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="headlessEditor" type="LexicalEditor">
    Your headless Lexical editor.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

#### doc.toJSON

Returns a serialized JSON object representation of your document. See Lexical’s
[Serialization & Deserialization](https://lexical.dev/docs/concepts/serialization)
page for more information.

```ts
const docAsJSON = await withLexicalDocument(
  { roomId: "my-room-id", client: liveblocks },
  // +++
  async (doc) => {
    return doc.toJSON();
  }
  // +++
);
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="docAsJson"
    type="SerializedEditorState<SerializedLexicalNode>"
  >
    A serialized JSON object representation of your document.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

#### doc.toMarkdown

Returns a markdown `string` of your document. See Lexical’s
[@lexical/markdown](https://lexical.dev/docs/concepts/serialization) page for
more information.

```ts
const markdown = await withLexicalDocument(
  { roomId: "my-room-id", client: liveblocks },
  // +++
  async (doc) => {
    return doc.toMarkdown();
  }
  // +++
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="markdown" type="string">
    Returns the markdown string.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

---
meta:
  title: "@liveblocks/node-prosemirror"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/node-prosemirror package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/node-prosemirror` provides a Node.js package to export and modify
[ProseMirror](https://prosemirror.net/). Because Tiptap uses ProseMirror under
the hood, this package can be used to modify
[Tiptap](/docs/api-reference/liveblocks-react-tiptap) documents as well.

## withProsemirrorDocument

`withProsemirrorDocument` is the main entry point to modifying a document on the
server. It takes a room ID and a
[Liveblocks Node client](/docs/api-reference/liveblocks-node#Liveblocks-client),
and returns a callback used to work with ProseMirror documents stored in
Liveblocks.

```ts highlight="8-14"
import { Liveblocks } from "@liveblocks/node";
import { withProsemirrorDocument } from "@liveblocks/node-prosemirror";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

await withProsemirrorDocument(
  { roomId: "your-room-id", client: liveblocks },
  (api) => {
    // Modify your document with the api
    // ...
  }
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="returns" type="T">
    Returns the value you return from the `api` callback.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="roomId" type="string" required>
    The ID of the room to use.
  </PropertiesListItem>
  <PropertiesListItem name="client" type="Liveblocks" required>
    The [Liveblocks
    client](/docs/api-reference/liveblocks-node#Liveblocks-client) to use.
  </PropertiesListItem>
  <PropertiesListItem name="schema" type="Schema">
    Optional. The ProseMirror schema to use for the document. If no schema is
    provided, the default schema is [Tiptap
    StarterKit](https://tiptap.dev/docs/editor/extensions/functionality/starterkit),
    Liveblocks mentions, and Liveblocks comments.
  </PropertiesListItem>
  <PropertiesListItem name="field" type="string">
    Optional. The
    [field](/docs/api-reference/liveblocks-react-tiptap#Multiple-editors) to use
    for the document. Defaults to `default`.
  </PropertiesListItem>
</PropertiesList>

### Returning data

Get your editor’s text content by returning `api.getText()` inside the callback.

```ts
const textContent = await withProsemirrorDocument(
  { roomId: "my-room-id", client: liveblocks },
  (api) => api.getText()
);

// "My content"
console.log(textContent);
```

### ProseMirror document API

You can easily modify your document with the ProseMirror document API.

#### api.update

Liveblocks provides `api.update` which is a callback that provides the current
document and a ProseMirror transaction. This makes it easy to use ProseMirror’s
built in functions. When you've finished, return the transaction and any changes
will be persisted, and appear in realtime to connected users as soon as the
`update` promise resolves.

```ts
await withProsemirrorDocument(
  {
    client,
    roomId: "test-room",
  },
  async (api) => {
    // +++
    await api.update((doc, tr) => {
      return tr.insertText("Hello world");
    });
    // +++
  }
);
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    type="(doc: Node, tr: Transaction) => Transaction"
  >
    `doc` is the ProseMirror document. `tr` is an editor state transaction.
    Transaction is a subclass of ProseMirror’s Transforms. On the ProseMirror
    website you can find a full list of
    [transforms](https://prosemirror.net/docs/ref/#transform.Document_transforms)
    and [transactions
    functions](https://prosemirror.net/docs/ref/#state.Transaction).
  </PropertiesListItem>
</PropertiesList>

#### api.getText

Returns the text content of the document. This API uses Tiptap’s `getText`
internally. TextSerializers are a concept from
[Tiptap](https://github.com/ueberdosis/tiptap/blob/3e59097b34ce8bc8c39e1def67eb31a1d9f9e5c2/packages/core/src/types.ts#L357).
If you are having trouble with a ProseMirror document, you may want to use
`api.getEditorState().doc.textBetween()` instead.

```ts
const textContent = await withProsemirrorDocument(
  { roomId: "my-room-id", client: liveblocks },
  async (api) => {
    // +++
    return api.getText({
      // Options
      // ...
    });
    // +++
  }
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="content" type="string">
    Returns the text retrieved from the document.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="options.blockSeparator" type="string">
    Optional. The separator to use for blocks, e.g. `<br />
    `. Defaults to `\n\n`.
  </PropertiesListItem>
  <PropertiesListItem name="options.textSerializers" type="string">
    Optional. The serializers to use for text. Defaults to `{}`.
  </PropertiesListItem>
</PropertiesList>

#### api.setContent

For convenience, some methods such as `setContent` are provided at the API
level. Here’s an example that sets a document and returns the JSON content after
it has been updated.

```ts
const exampleDoc = {
  type: "doc",
  content: [
    {
      type: "paragraph",
      content: [
        {
          type: "text",
          text: "Example Text",
        },
      ],
    },
  ],
};

const json = await withProsemirrorDocument<string>(
  {
    client,
    roomId: "test-room",
  },
  async (api) => {
    // +++
    await api.setContent(exampleDoc);
    // +++
    return JSON.stringify(api.toJSON());
  }
);
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesList title="Arguments">
  <PropertiesListItem name="content" type="null | object | string" required>
    The content to replace your document.
  </PropertiesListItem>
</PropertiesList>

#### api.getEditorState

Returns the current ProseMirror state.

```ts
const editorState = await withProsemirrorDocument(
  { roomId: "my-room-id", client: liveblocks },
  async (api) => {
    // +++
    return api.getEditorState();
    // +++
  }
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="editorState" type="EditorState">
    Your editor’s ProseMirror state.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

#### api.toJSON

Returns a serialized JSON object representation of your document. See
ProseMirror’s
[.toJSON](https://prosemirror.net/docs/ref/#state.EditorState.toJSON)
documentation for more information.

```ts
const docAsJSON = await withProsemirrorDocument(
  { roomId: "my-room-id", client: liveblocks },
  async (api) => {
    // +++
    return api.toJSON();
    // +++
  }
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="editorState" type="string">
    Your editor’s serialized JSON state.
  </PropertiesListItem>
</PropertiesList>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

#### api.clearContent

Clears the content of the document.

```ts
await withProsemirrorDocument(
  { roomId: "my-room-id", client: liveblocks },
  async (api) => {
    // +++
    return api.clearContent();
    // +++
  }
);
```

<PropertiesListEmpty title="Returns">_Nothing_</PropertiesListEmpty>

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

#### api.toMarkdown

Returns a markdown `string` of your document.

```ts
const markdown = await withProsemirrorDocument(
  { roomId: "my-room-id", client: liveblocks },
  async (api) => {
    // +++
    return api.toMarkdown();
    // +++
  }
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="markdown" type="string">
    Returns the markdown string.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="serializer" type="MarkdownSerializer">
    Optional. A markdown serializer to use. By default it uses the
    `defaultMarkdownSerializer` from
    [prosemirror-markdown](https://github.com/prosemirror/prosemirror-markdown).
  </PropertiesListItem>
</PropertiesList>

##### Custom markdown serializer

You can use a custom markdown serializer.

```ts
import { defaultMarkdownSerializer } from "prosemirror-markdown";

const mySerializer = new MarkdownSerializer({
  marks: {
    ...defaultMarkdownSerializer.marks,
    em: {
      open: "*",
      close: "*",
      mixable: true,
      expelEnclosingWhitespace: true,
    },
  },
});

const markdown = await withProsemirrorDocument(
  { roomId: "my-room-id", client: liveblocks },
  async (api) => {
    // +++
    return api.toMarkdown(mySerializer);
    // +++
  }
);
```

---
meta:
  title: "@liveblocks/node"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/node package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/node` provides you with Node.js APIs for
[authenticating Liveblocks users](#Liveblocks-client) and for
[implementing webhook handlers](#WebhookHandler). This library is only intended
for use in your Node.js back end.

## Liveblocks client [#Liveblocks-client]

The `Liveblocks` client offers access to our REST API.

```ts showLineNumbers={false}
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});
```

### Authentication [#Authorization]

To authenticate your users with Liveblocks, you have the choice between two
different APIs.

- [`Liveblocks.identifyUser`](#id-tokens) ID token authentication is recommend
  for most applications.
- [`Liveblocks.prepareSession`](#access-tokens) Access token authentication is
  best if you prefer handle permissions on your end, though it has
  [limitations](/docs/authentication/access-token#limitations).

#### Liveblocks.identifyUser [#id-tokens]

Creates an ID token that is used to authenticate a user in your application.
This is a wrapper around the
[Get ID Token API](/docs/api-reference/rest-api-endpoints#post-identify-user)
and returns the same response.

```ts
const { body, status } = await liveblocks.identifyUser({
  // Required, the current user's ID
  userId: "marie@example.com",
});
```

<Banner>
  Learn how to [get started with ID tokens](/docs/authentication).
</Banner>

A number of options are also available, enabling you to set up permissions and
user metadata.

```ts
const { body, status } = await liveblocks.identifyUser(
  {
    // Required, the current user's ID
    userId: "marie@example.com",

    // Optional, only view resources on this tenant
    tenantId: "acme-corp",

    // Optional, used to provision room access on group level
    groupIds: ["marketing", "engineering"],
  },
  {
    // Optional, custom user metadata
    userInfo: {
      name: "Marie",
      color: "#00ff00",
      avatar: "https://example.com/avatar/marie.jpg",
    },
  }
);
```

##### Granting ID token permissions

You can pass additional options to `identifyUser`, enabling you to create
complex [workspace permissions](/docs/authentication#permissions) and
[room permissions](/docs/authentication#Room-permissions). For example, this
user can only see resources in the `acme-corp` workspace, and they’re part of a
`marketing` rooms group within it.

```ts
const { body, status } = await liveblocks.identifyUser({
  // Required, the current user's ID
  userId: "marie@example.com",

  // Optional, only view resources on this workspace
  // +++
  tenantId: "acme-corp",
  // +++

  // Optional, used to provision room access on group level
  // +++
  groupIds: ["marketing"],
  // +++
});
```

<Banner>
  Learn more about [ID token permissions](/docs/authentication#permissions).
</Banner>

##### Text editor user data

When using
[text editor integrations](/docs/ready-made-features/multiplayer#Text-editor-integrations),
user data is inserted into their live cursor within the editor, showing their
name and color. This data originates from the `userInfo` property.

```ts
const { body, status } = await liveblocks.identifyUser(
  {
    // Required, the current user's ID
    userId: "marie@example.com",
  },
  {
    // Optional, custom user metadata
    userInfo: {
      // Used in text editor live carets
      // +++
      name: "Marie",
      color: "#00ff00",
      // +++
    },
  }
);
```

##### Custom user metadata

You can pass additional options to `prepareSession`, enabling you to add custom
user metadata to the session. This metadata can be accessed by all users in the
room, and is useful for building features such as live avatar stacks.

```ts
const { body, status } = await liveblocks.identifyUser(
  {
    // Required, the current user's ID
    userId: "marie@example.com",
  },
  {
    // Optional, custom user metadata
    userInfo: {
      // Add custom properties to use on front end, e.g. avatar stacks
      // +++
      avatar: "https://example.com/avatar/marie.jpg",
      // +++
      // ...
    },
  }
);
```

To access it on the front end, use hooks such as
[`useSelf`](/docs/api-reference/liveblocks-react#useSelf) and
[`useOthers`](/docs/api-reference/liveblocks-react#useOthers).

```tsx
const currentUser = useSelf();

// "https://example.com/avatar/marie.jpg"
console.log(currentUser.info.avatar);
```

##### How ID tokens work

The purpose of this API is to help you implement your custom authentication back
end (i.e. the _server_ part of the diagram). You use the
`liveblocks.identifyUser()` API if you’d like to issue
[ID tokens](/docs/authentication/id-token) from your back end. An ID token does
not grant any permissions in the token directly. Instead, it only securely
identifies your user, and then uses any permissions set via the [Permissions
REST API][] to decide whether to allow the user on a room-by-room basis.

Use this approach if you’d like Liveblocks to be the source of truth for your
user’s permissions.

<Banner title="What are ID tokens?">
  Issuing identity tokens is like issuing _membership cards_. Anyone with a
  membership card can try to enter a room, but your permissions will be checked
  at the door. The Liveblocks servers perform this authorization, so your
  permissions need to be set up front using the Liveblocks REST API.
</Banner>

<Figure>
  <Image
    src="/assets/id-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

Implement your back end endpoint as follows:

```ts showLineNumbers={false}
const { body, status } = await liveblocks.identifyUser(
  {
    userId: "marie@example.com", // Required, user ID from your DB
    groupIds: ["marketing", "engineering"],
    // Optional, identify the user in a specific tenant
    tenantId: "acme-corp",
  },

  // Optional
  {
    userInfo: {
      name: "Marie",
      avatar: "https://example.com/avatar/marie.jpg",
    },
  }
);

return new Response(body, { status });
```

`userId` (required) is a string identifier to uniquely identify your user with
Liveblocks. This value will be used when counting unique MAUs in your Liveblocks
dashboard. You can refer to these user IDs in the [Permissions REST API][] when
assigning group permissions.

`groupIds` (optional) can be used to specify which groups this user belongs to.
These are arbitrary identifiers that make sense to your app, and that you can
refer to in the [Permissions REST API][] when assigning group permissions.

`tenantId` (optional) is the tenant for this user, will be set to `default` if
not provided.

`userInfo` (optional) is any custom JSON value, which you can use to attach
static metadata to this user’s session. This will be publicly visible to all
other people in the room. Useful for metadata like the user’s full name, or
their avatar URL.

##### ID tokens example

Here’s a real-world example of ID tokens in a Next.js route handler/endpoint.
You can find examples for other frameworks in our
[authentication section](/docs/authentication/id-token).

```ts file="Next.js"
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export default async function auth(req, res) {
  /**
   * Implement your own security here.
   *
   * It's your responsibility to ensure that the caller of this endpoint
   * is a valid user by validating the cookies or authentication headers
   * and that it has access to the requested room.
   */

  // Get the current user from your database
  const user = __getUserFromDB__(req);

  // Create an ID token for the user
  const { body, status } = await liveblocks.identifyUser(
    {
      userId: user.id,
    },
    {
      userInfo: {
        name: user.fullName,
        color: user.favoriteColor,
      },
    }
  );

  return new Response(body, { status });
}
```

#### Liveblocks.prepareSession [#access-tokens]

Creates an access token that is used to authenticate a user in your application.
This is a wrapper around the
[Get Access Token API](/docs/api-reference/rest-api-endpoints#post-authorize-user)
and returns the same response.

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com"
);
```

<Banner>
  Learn how to [get started with access
  tokens](/docs/authentication/access-token).
</Banner>

A number of options are also available, enabling you to set up permissions and
user metadata.

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com",
  {
    // Optional, only view resources on this tenant
    tenantId: "acme-corp",

    // Optional, used to provision room access on group level
    groupIds: ["marketing"],

    // Optional, custom user metadata
    userInfo: {
      name: "Marie",
      color: "#00ff00",
      avatar: "https://example.com/avatar/marie.jpg",
    },
  }
);
```

##### Granting access token permissions

Using `session.allow()`, you can grant full or read-only permissions to the user
to select rooms. Wildcards can be used to enable granting permissions to
multiple rooms at once using
[naming patterns](/docs/authentication/access-token#Naming-pattern).

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com"
);

// Giving access to an individual rooms
session.allow("room-id-1", session.FULL_ACCESS);

// Giving read-only access to this room
session.allow("room-id-2", session.READ_ACCESS);

// Giving access to multiple rooms with a wildcard
// `design-room-1`, `design-room-2`, etc.
session.allow("design-room:*", session.FULL_ACCESS);
```

<Banner>
  Learn more about [access token
  permissions](/docs/authentication/access-token#permissions).
</Banner>

Additionally, you can pass additional options to `prepareSession`, enabling you
to create complex permissions using [tenants](/docs/authentication/tenants) and
[accesses](/docs/authentication/access-tokens/permissions). For example, this
user can only see resources in the `acme-corp` tenant, and they’re part of a
`marketing` group within it.

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com",
  {
    // Optional, only view resources on this tenant
    // +++
    tenantId: "acme-corp",
    // +++

    // Optional, used to provision room access on group level
    // +++
    groupIds: ["marketing"],
    // +++
  }
);
```

##### Text editor user data

When using
[text editor integrations](/docs/ready-made-features/multiplayer#Text-editor-integrations),
user data is inserted into their live cursor within the editor, showing their
name and color. This data originates from the `userInfo` property of the
session.

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com",
  {
    // Optional, user metadata
    userInfo: {
      // Used in text editor live carets
      // +++
      name: "Marie",
      color: "#00ff00",
      // +++
    },
  }
);
```

##### Custom user metadata

You can pass additional options to `prepareSession`, enabling you to add custom
user metadata to the session. This metadata can be accessed by all users in the
room, and is useful for building features such as live avatar stacks.

```ts
const session = liveblocks.prepareSession(
  // Required, the current user's ID
  "marie@example.com",
  {
    // Optional, custom user metadata
    userInfo: {
      // Add custom properties to use on front end, e.g. avatar stacks
      // +++
      avatar: "https://example.com/avatar/marie.jpg",
      // +++
      // ...
    },
  }
);
```

To access it on the front end, use hooks such as
[`useSelf`](/docs/api-reference/liveblocks-react#useSelf) and
[`useOthers`](/docs/api-reference/liveblocks-react#useOthers).

```tsx
const currentUser = useSelf();

// "https://example.com/avatar/marie.jpg"
console.log(currentUser.info.avatar);
```

##### How access tokens work

The purpose of this API is to help you implement your custom authentication back
end (i.e. the _server_ part of the diagram). You use the
`liveblocks.prepareSession()` API if you’d like to issue
[access tokens](/docs/authentication/access-token) from your back end.

<Banner title="What are access tokens?">
  Issuing access tokens is like issuing _hotel key cards_ from a hotel’s front
  desk (your back end). Any client with a key card can enter any room that the
  card gives access to. It’s easy to give out those key cards right from your
  back end.
</Banner>

<Figure>
  <Image
    src="/assets/access-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

To implement your back end, follow these steps:

<Steps>
  <Step>
    <StepTitle>Create a session</StepTitle>

    <StepContent>
      ```ts showLineNumbers={false}
      const session = liveblocks.prepareSession(
        "marie@example.com",   // Required, user ID from your DB
        {
          // Optional, custom static metadata for the session
          userInfo: {
            name: "Marie",
            avatar: "https://example.com/avatar/marie.jpg",
          },
          // Optional, authenticate this user on a specific tenant
          tenantId: "acme-corp",
        }
      );
      ```

      The `userId` (required) is an identifier to uniquely identifies
      your user with Liveblocks. This value will be used when counting
      unique MAUs in your Liveblocks dashboard.

      The `userInfo` (optional) is any custom JSON value, which can be
      attached to static metadata to this user’s session. This will be
      publicly visible to all other people in the room. Useful for
      metadata like the user’s full name, or their avatar URL.

      The `tenantId` (optional) is the tenant for this session, will be set to `default` if not provided.

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Decide which permissions to allow this session</StepTitle>
    <StepContent>
      ```ts showLineNumbers={false}
      session.allow("my-room-1", session.FULL_ACCESS);
      session.allow("my-room-2", session.FULL_ACCESS);
      session.allow("my-room-3", session.FULL_ACCESS);
      session.allow("my-team:*", session.READ_ACCESS);
      ```

      <Banner title="Be diligent" type="warning">
        You’re specifying what’s going to be allowed so be careful what
        permissions you’re giving your users. You’re responsible for this
        part.
      </Banner>
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Authorize the session</StepTitle>
    <StepContent>
      Finally, authorize the session. This step makes the HTTP call to the
      Liveblocks servers. Liveblocks will return a signed **access token** that
      you can return to your client.

      ```ts showLineNumbers={false}
      // Requests the Liveblocks servers to authorize this session
      const { body, status } = await session.authorize();
      return new Response(body, { status });
      ```
    </StepContent>

  </Step>

</Steps>

##### Access tokens example [#access-token-example]

Here’s a real-world example of access tokens in a Next.js route
handler/endpoint. You can find examples for other frameworks in our
[authentication section](/docs/authentication/access-token).

```ts file="route.ts"
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST(request: Request) {
  /**
   * Implement your own security here.
   *
   * It's your responsibility to ensure that the caller of this endpoint
   * is a valid user by validating the cookies or authentication headers
   * and that it has access to the requested room.
   */

  // Get the current user from your database
  const user = __getUserFromDB__(request);

  // Start an auth session inside your endpoint
  const session = liveblocks.prepareSession(
    user.id,
    { userInfo: user.metadata } // Optional
  );

  // Implement your own security, and give the user access to the room
  const { room } = await request.json();
  if (room && __shouldUserHaveAccess__(user, room)) {
    session.allow(room, session.FULL_ACCESS);
  }

  // Retrieve a token from the Liveblocks servers and pass it to the
  // requesting client
  const { body, status } = await session.authorize();
  return new Response(body, { status });
}
```

### Room

#### Liveblocks.getRooms [#get-rooms]

Returns a list of rooms that are in the current project. The project is
determined by the secret key you’re using. Rooms are sorted by creation time,
with the newest room at index `0`. This is a wrapper around the
[Get Rooms API](/docs/api-reference/rest-api-endpoints#get-rooms) and returns
the same response.

```ts
const { data: rooms, nextCursor } = await liveblocks.getRooms();

// A list of rooms
// [{ type: "room", id: "my-room-id", ... }, ...]
console.log(rooms);

// A pagination cursor used for retrieving the next page of results with `startingAfter`
// "L3YyL3Jvb21z..."
console.log(nextCursor);
```

A number of options are also available, enabling you to filter for certain
rooms.

```ts
const { data: rooms, nextCursor } = await liveblocks.getRooms({
  // Optional, the amount of rooms to load, between 1 and 100, defaults to 20
  limit: 20,

  // Optional, filter for rooms that allow entry to group ID(s) in `groupsAccesses`
  groupIds: ["engineering", "design"],

  // Optional, filter for rooms that allow entry to a user's ID in `usersAccesses`
  userId: "my-user-id",

  // Optional, use advanced filtering
  query: {
    // Optional, filter for rooms with an ID that starts with specific string
    roomId: {
      startsWith: "liveblocks:",
    },
    // Optional, filter for rooms with custom metadata in `metadata`
    metadata: {
      roomType: "whiteboard",
    },
  },

  // Optional, authenticate this user on a specific tenant
  tenantId: "my-tenant-id",

  // Optional, cursor used for pagination, use `nextCursor` from the previous page's response
  startingAfter: "L3YyL3Jvb21z...",
});
```

The `query` option also allows you to pass a
[query language](/docs/guides/how-to-filter-rooms-using-query-language) string
instead of a `query` object.

##### Pagination

You can use `nextCursor` to paginate rooms. In this example, when `getNextPage`
is called, the next set of rooms is added to `pages`.

```ts
import { RoomData } from "@liveblocks/node";

// An array of pages, each containing a list of retrieved rooms
const pages: RoomData[][] = [];

// Holds the pagination cursor for the next set of rooms
let startingAfter;

// Call to get the next page of rooms
async function getNextPage() {
  const { data, nextCursor } = await liveblocks.getRooms({ startingAfter });
  pages.push(data);
  startingAfter = nextCursor;
}
```

If you’d like to iterate over all your rooms, it’s most convenient to use
[`liveblocks.iterRooms`](#iter-rooms) instead. This method automatically
paginates your API requests.

#### Liveblocks.iterRooms [#iter-rooms]

Works similarly to [`liveblocks.getRooms`](#get-rooms), but instead returns an
asynchronous iterator, which helps you iterate over all selected rooms in your
project, without having to manually paginate through the results.

```ts
const roomsIterator = liveblocks.iterRooms();

for await (const room of roomsIterator) {
  // { type: "room", id: "my-room-id", metadata: {...}, ... }
  console.log(room);
}
```

A number of options are also available, enabling you to filter for certain
rooms.

```ts
const roomsIterator = await liveblocks.iterRooms({
  // Optional, filter for rooms that allow entry to group ID(s) in `groupsAccesses`
  groupIds: ["engineering", "design"],

  // Optional, filter for rooms that allow entry to a user's ID in `usersAccesses`
  userId: "my-user-id",

  // Optional, use advanced filtering
  query: {
    // Optional, filter for rooms with an ID that starts with specific string
    roomId: {
      startsWith: "liveblocks:",
    },
    // Optional, filter for rooms with custom metadata in `metadata`
    metadata: {
      roomType: "whiteboard",
    },
  },
});

for await (const room of roomsIterator) {
  // { type: "room", id: "my-room-id", metadata: {...}, ... }
  console.log(room);
}
```

The `query` option also allows you to pass a
[query language](/docs/guides/how-to-filter-rooms-using-query-language) string
instead of a `query` object.

##### Mass deleting rooms

You can use `iterRooms` to efficiently delete multiple rooms at once. This
example shows how to delete rooms in batches of 50 concurrent deletions at a
time:

```ts
const MAX_CONCURRENT = 50;
const queue: Promise<void>[] = [];

for await (const room of liveblocks.iterRooms({
  // Optionally filter for certain rooms
  // ...
})) {
  if (queue.length >= MAX_CONCURRENT) {
    await Promise.race(queue);
  }

  const promise = liveblocks
    .deleteRoom(room.id)
    .finally(() => queue.splice(queue.indexOf(promise), 1));

  queue.push(promise);
}

await Promise.all(queue);
```

This approach is useful when you need to delete a large number of rooms, as it
automatically handles pagination and allows you to control the concurrency of
deletions. You can use any of the filtering options shown above to select which
rooms to delete.

#### Liveblocks.createRoom [#post-rooms]

Programmatically creates a new room from a room ID. The `defaultAccesses` option
is required. Setting `defaultAccesses` to `["room:write"]` creates a public
room, whereas setting it to `[]` will create a private room that needs
[ID token permission to enter](/docs/authentication/id-token). This is a wrapper
around the [Create Room API](/docs/api-reference/rest-api-endpoints#post-rooms)
and returns the same response.

```ts
const room = await liveblocks.createRoom("my-room-id", {
  defaultAccesses: ["room:write"],
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

A number of room creation options are available, allowing you to set permissions
and attach custom metadata.

```ts
const room = await liveblocks.createRoom("my-room-id", {
  // The default room permissions. `[]` for private, `["room:write"]` for public.
  defaultAccesses: [],

  // Optional, the room's group ID permissions
  groupsAccesses: {
    design: ["room:write"],
    engineering: ["room:presence:write", "room:read"],
  },

  // Optional, the room's user ID permissions
  usersAccesses: {
    "my-user-id": ["room:write"],
  },

  // Optional, custom metadata to attach to the room
  metadata: {
    myRoomType: "whiteboard",
  },

  // Optional, create it on a specific tenant
  tenantId: "acme-corp",
});
```

Group and user permissions are only used with
[ID token authorization](/docs/api-reference/liveblocks-node#id-tokens), learn
more about [managing permission with ID tokens](/docs/authentication/id-token).

#### Liveblocks.getRoom [#get-rooms-roomId]

Returns a room. Throws an error if the room isn’t found. This is a wrapper
around the
[Get Room API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId) and
returns the same response.

```ts
const room = await liveblocks.getRoom("my-room-id");

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

#### Liveblocks.getOrCreateRoom [#get-or-create-rooms-roomId]

Get a room by its ID. If the room doesn’t exist, create it instead. The
`defaultAccesses` option is required. Setting `defaultAccesses` to
`["room:write"]` creates a public room, whereas setting it to `[]` will create a
private room that needs
[ID token permission to enter](/docs/authentication/id-token). Returns the same
response as the
[Create Room API](/docs/api-reference/rest-api-endpoints#post-rooms).

```ts
const room = await liveblocks.getOrCreateRoom("my-room-id", {
  defaultAccesses: ["room:write"],
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

A number of room creation options are available, allowing you to set permissions
and attach custom metadata.

```ts
const room = await liveblocks.getOrCreateRoom("my-room-id", {
  // The default room permissions. `[]` for private, `["room:write"]` for public.
  defaultAccesses: [],

  // Optional, the room's group ID permissions
  groupsAccesses: {
    design: ["room:write"],
    engineering: ["room:presence:write", "room:read"],
  },

  // Optional, the room's user ID permissions
  usersAccesses: {
    "my-user-id": ["room:write"],
  },

  // Optional, custom metadata to attach to the room
  metadata: {
    myRoomType: "whiteboard",
  },

  // Optional, create it on a specific tenant
  tenantId: "acme-corp",
});
```

Group and user permissions are only used with
[ID token authorization](/docs/api-reference/liveblocks-node#id-tokens), learn
more about [managing permission with ID tokens](/docs/authentication/id-token).

#### Liveblocks.updateRoom [#post-rooms-roomId]

Updates properties on a room. Throws an error if the room isn’t found. This is a
wrapper around the
[Update Room API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId) and
returns the same response.

```ts
const room = await liveblocks.updateRoom("my-room-id", {
  // The metadata or permissions you're updating
  // ...
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

Permissions and metadata properties can be updated on the room. Note that you
need only pass the properties you’re updating. Setting a property to `null` will
delete the property.

```ts
const room = await liveblocks.updateRoom("my-room-id", {
  // Optional, update the default room permissions. `[]` for private, `["room:write"]` for public.
  defaultAccesses: [],

  // Optional, update the room's group ID permissions
  groupsAccesses: {
    design: ["room:write"],
    engineering: ["room:presence:write", "room:read"],
  },

  // Optional, update the room's user ID permissions
  usersAccesses: {
    "my-user-id": ["room:write"],
  },

  // Optional, custom metadata to update on the room
  metadata: {
    myRoomType: "whiteboard",
  },
});
```

Group and user permissions are only used with
[ID token authorization](/docs/api-reference/liveblocks-node#id-tokens), learn
more about [managing permission with ID tokens](/docs/authentication/id-token).

#### Liveblocks.upsertRoom [#upsert-rooms-roomId]

Update a room’s properties by its ID. If the room doesn’t exist, create it
instead. The `defaultAccesses` option is required. Setting `defaultAccesses` to
`["room:write"]` creates a public room, whereas setting it to `[]` will create a
private room that needs
[ID token permission to enter](/docs/authentication/id-token). Returns the same
response as the
[Create Room API](/docs/api-reference/rest-api-endpoints#post-rooms).

```ts
const room = await liveblocks.upsertRoom("my-room-id", {
  // These fields will get updated when the room exists, or will be created
  update: {
    metadata: { color: "red" },
  },
  // These fields will only be set when the room will get created
  create: {
    defaultAccesses: ["room:write"],
  },
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

A number of room update or creation options are available, allowing you to set
permissions and attach custom metadata.

```ts
const room = await liveblocks.upsertRoom("my-room-id", {
  update: {
    // The default room permissions. `[]` for private, `["room:write"]` for public.
    defaultAccesses: [],

    // Optional, the room's group ID permissions
    groupsAccesses: {
      design: ["room:write"],
      engineering: ["room:presence:write", "room:read"],
    },

    // Optional, the room's user ID permissions
    usersAccesses: {
      "my-user-id": ["room:write"],
    },

    // Optional, custom metadata to attach to the room
    metadata: {
      myRoomType: "whiteboard",
    },
  },
});
```

Group and user permissions are only used with
[ID token authorization](/docs/api-reference/liveblocks-node#id-tokens), learn
more about [managing permission with ID tokens](/docs/authentication/id-token).

#### Liveblocks.deleteRoom [#delete-rooms-roomId]

Deletes a room. If the room doesn’t exist, or has already been deleted, no error
will throw. This is a wrapper around the
[Delete Room API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId)
and returns no response.

```ts
await liveblocks.deleteRoom("my-room-id");
```

##### Mass deleting rooms

If you need to delete multiple rooms at once, you can use
[`liveblocks.iterRooms`](#iter-rooms) to efficiently iterate through rooms and
delete them in batches. This example shows how to delete rooms in batches of 50
concurrent deletions at a time:

```ts
const MAX_CONCURRENT = 50;
const queue: Promise<void>[] = [];

for await (const room of liveblocks.iterRooms({
  // Optionally filter for certain rooms
  // ...
})) {
  if (queue.length >= MAX_CONCURRENT) {
    await Promise.race(queue);
  }

  const promise = liveblocks
    .deleteRoom(room.id)
    .finally(() => queue.splice(queue.indexOf(promise), 1));

  queue.push(promise);
}

await Promise.all(queue);
```

You can use any of the filtering options available in
[`liveblocks.iterRooms`](#iter-rooms) to select which rooms to delete, such as
filtering by metadata, room ID prefix, or user/group access.

#### Liveblocks.prewarmRoom [#get-rooms-roomId-prewarm]

Speeds up connecting to a room for the next 10 seconds. Use this when you know a
user will be connecting to a room with
[`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider) or
[`enterRoom`](/docs/api-reference/liveblocks-client#Client.enterRoom) within 10
seconds, and the room will load quicker. This is a wrapper around the
[Prewarm Room API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-prewarm)
and returns no response.

```ts
await liveblocks.prewarmRoom("my-room-id");
```

##### Warm a room before navigating

Triggering a room directly before a user navigates to a room is an easy to way
use this API. Here’s a Next.js server actions example, showing how to trigger
prewarming with `onPointerDown`.

```ts title="actions.ts"
"use server";

import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function prewarmRoom(roomId: string) {
  // +++
  await liveblocks.prewarmRoom(roomId);
  // +++
}
```

```tsx title="RoomLink.tsx"
"use client";

import { prewarmRoom } from "../actions";
import Link from "next/link";

export function JoinButton({ roomId }: { roomId: string }) {
  return (
    // +++
    <Link href={`/rooms/${roomId}`} onPointerDown={() => prewarmRoom(roomId)}>
      // +++
      {roomId}
    </Link>
  );
}
```

`onPointerDown` is slightly quicker than `onClick` because it triggers before
the user releases their pointer.

#### Liveblocks.updateRoomId [#post-rooms-update-roomId]

Permanently updates a room’s ID. `newRoomId` will replace `currentRoomId`. Note
that this will disconnect connected users from the room, but this can be worked
around. Throws an error if the room isn’t found. This is a wrapper around the
[Update Room API](/docs/api-reference/rest-api-endpoints#post-rooms-update-roomId)
and returns the same response.

```ts
const room = await liveblocks.updateRoomId({
  currentRoomId: "my-room-id",
  newRoomId: "new-room-id",
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

##### Redirect connected users to the new room

When a room’s ID is changed it disconnects all users that are currently
connected. To redirect connected users to the new room you can use
[`useErrorListener`](/docs/api-reference/liveblocks-react#useErrorListener) or
[`room.subscribe("error")`](/docs/api-reference/liveblocks-client#Room.subscribe.error)
in your application to get the new room’s ID, and redirect users to the renamed
room.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

function App() {
  useErrorListener((error) => {
    if (error.context.code === 4006) {
      // Room ID has been changed, get the new ID and redirect
      const newRoomId = error.message;
      __redirect__(`https://example.com/document/${newRoomId}}`);
    }
  });
}
```

#### Liveblocks.getActiveUsers [#get-rooms-roomId-active-users]

Returns a list of users that are currently present in the room. Throws an error
if the room isn’t found. This is a wrapper around the
[Get Active Users API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-active-users)
and returns the same response.

```ts
const activeUsers = await liveblocks.getActiveUsers("my-room-id");

// { data: [{ type: "user", id: "my-user-id", ... }, ...] }
console.log(activeUsers);
```

#### Liveblocks.broadcastEvent [#post-broadcast-event]

Broadcasts a custom event to the room. Throws an error if the room isn’t found.
This is a wrapper around the
[Broadcast Event API](/docs/api-reference/rest-api-endpoints#post-broadcast-event)
and returns no response.

```ts
const customEvent = {
  type: "EMOJI",
  emoji: "🔥",
};

await liveblocks.broadcastEvent("my-room-id", customEvent);
```

You can respond to custom events on the front end with
[`useEventListener`](/docs/api-reference/liveblocks-react#useEventListener) and
[`room.subscribe("event")`](/docs/api-reference/liveblocks-client#Room.subscribe.event).
When receiving an event sent with `Liveblocks.broadcastEvent`, `user` will be
`null` and `connectionId` will be `-1`.

```tsx
import { useEventListener } from "@liveblocks/react/suspense";

// When receiving an event sent from `@liveblocks/node`
useEventListener(({ event, user, connectionId }) => {
  // `null`
  console.log(user);

  // `-1`
  console.log(connectionId);
});
```

### Groups

Groups allow you to manage users for group mentions in comments and text
editors.

#### Liveblocks.createGroup [#create-group]

Creates a new group with the specified members. This is a wrapper around the
[Create Group API](/docs/api-reference/rest-api-endpoints#post-groups) and
returns the same response.

```ts
const group = await liveblocks.createGroup({
  groupId: "engineering-team",
  memberIds: ["alice@example.com", "bob@example.com"],
});

// { type: "group", id: "engineering-team", tenantId: "acme-corp", createdAt: "...", updatedAt: "...", scopes: { mention: true }, members: [...] }
console.log(group);
```

You can also create a group without members and add them later:

```ts
const group = await liveblocks.createGroup({
  groupId: "design-team",
  // Optional, add members when creating the group
  memberIds: ["charlie@example.com"],

  // Optional, create it on a specific tenant
  tenantId: "company-123",

  // Optional, set group scopes (defaults to { mention: true })
  scopes: { mention: true },
});
```

#### Liveblocks.getGroup [#get-group]

Returns a group by its ID. Throws an error if the group isn’t found. This is a
wrapper around the
[Get Group API](/docs/api-reference/rest-api-endpoints#get-groups-groupId) and
returns the same response.

```ts
const group = await liveblocks.getGroup({
  groupId: "engineering-team",
});

// { type: "group", id: "engineering-team", tenantId: "acme-corp", createdAt: "...", updatedAt: "...", scopes: { mention: true }, members: [...] }
console.log(group);
```

#### Liveblocks.getGroups [#get-groups]

Returns a list of all groups in your project. This is a wrapper around the
[Get Groups API](/docs/api-reference/rest-api-endpoints#get-groups) and returns
the same response.

```ts
const { data: groups, nextCursor } = await liveblocks.getGroups();

// A list of groups
// [{ type: "group", id: "engineering-team", tenantId: "acme-corp", createdAt: "...", updatedAt: "...", scopes: { mention: true }, members: [...] }, ...]
console.log(groups);

// A pagination cursor for the next page
console.log(nextCursor);
```

You can also paginate through groups:

```ts
const { data: groups, nextCursor } = await liveblocks.getGroups({
  // Optional, the number of groups to return (defaults to 20)
  limit: 50,

  // Optional, cursor for pagination
  startingAfter: nextCursor,
});
```

#### Liveblocks.getUserGroups [#get-user-groups]

Returns all groups that a specific user is a member of. This is a wrapper around
the
[Get User Groups API](/docs/api-reference/rest-api-endpoints#get-users-userId-groups)
and returns the same response.

```ts
const { data: userGroups, nextCursor } = await liveblocks.getUserGroups({
  userId: "alice@example.com",
});

// A list of groups the user belongs to
// [{ type: "group", id: "engineering-team", ... }, ...]
console.log(userGroups);
```

You can also paginate through user groups:

```ts
const { data: userGroups, nextCursor } = await liveblocks.getUserGroups({
  userId: "alice@example.com",
  limit: 25,
  startingAfter: "L3YyL2dyb3Vwcy...",
});
```

#### Liveblocks.addGroupMembers [#add-group-members]

Adds new members to an existing group. This is a wrapper around the
[Add Group Members API](/docs/api-reference/rest-api-endpoints#post-groups-groupId-add-members)
and returns the same response.

```ts
const updatedGroup = await liveblocks.addGroupMembers({
  groupId: "engineering-team",
  memberIds: ["david@example.com", "eve@example.com"],
});

// { type: "group", id: "engineering-team", tenantId: "acme-corp", createdAt: "...", updatedAt: "...", scopes: { mention: true }, members: [...] }
console.log(updatedGroup);
```

#### Liveblocks.removeGroupMembers [#remove-group-members]

Removes members from an existing group. This is a wrapper around the
[Remove Group Members API](/docs/api-reference/rest-api-endpoints#post-groups-groupId-remove-members)
and returns the same response.

```ts
const updatedGroup = await liveblocks.removeGroupMembers({
  groupId: "engineering-team",
  memberIds: ["david@example.com"],
});

// { type: "group", id: "engineering-team", tenantId: "acme-corp", createdAt: "...", updatedAt: "...", scopes: { mention: true }, members: [...] }
console.log(updatedGroup);
```

#### Liveblocks.deleteGroup [#delete-group]

Deletes a group. If the group doesn’t exist, no error will be thrown. This is a
wrapper around the
[Delete Group API](/docs/api-reference/rest-api-endpoints#delete-groups-groupId)
and returns no response.

```ts
await liveblocks.deleteGroup({
  groupId: "old-team",
});
```

### Storage

#### Liveblocks.getStorageDocument [#get-rooms-roomId-storage]

Returns the contents of a room’s Storage tree. By default, returns Storage in
LSON format. Throws an error if the room isn’t found. This is a wrapper around
the
[Get Storage Document API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-storage)
and returns the same response.

```ts
const storage = await liveblocks.getStorageDocument("my-room-id");
```

LSON is a custom Liveblocks format that preserves information about the
conflict-free data types used. By default, `getStorageDocument` returns Storage
in this format. This is the same as using `"plain-json"` in the second argument.

```ts highlight="2"
// Retrieve LSON Storage data
const storage = await liveblocks.getStorageDocument("my-room-id", "plain-lson");

// If this were your Storage type...
declare global {
  interface Liveblocks {
    Storage: {
      names: LiveList<string>;
    };
  }
}

// {
//   liveblocksType: "LiveObject",
//   data: {
//     names: {
//       liveblocksType: "LiveList",
//       data: ["Olivier", "Nimesh"],
//     }
//   }
// }
console.log(storage);
```

You can also retrieve Storage as JSON by passing `"json"` into the second
argument.

```ts highlight="2"
// Retrieve JSON Storage data
const storage = await liveblocks.getStorageDocument("my-room-id", "json");

// If this were your Storage type...
declare global {
  interface Liveblocks {
    Storage: {
      names: LiveList<string>;
    };
  }
}

// {
//   names: ["Olivier", "Nimesh"]
// }
console.log(storage);
```

#### Liveblocks.initializeStorageDocument [#post-rooms-roomId-storage]

Initializes a room’s Storage tree with given LSON data. To use this, the room
must have [already been created](#post-rooms) and have empty Storage. Throws an
error if the room isn’t found. Calling this will disconnect all active users
from the room. This is a wrapper around the
[Initialize Storage Document API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-storage)
and returns the same response.

```ts
// Create a new room
const room = await liveblocks.createRoom("my-room-id", {
  defaultAccesses: ["room:write"],
});

// Initialize Storage
const storage = await liveblocks.initializeStorageDocument("my-room-id", {
  // Your LSON Storage value
  // ...
});
```

LSON is a custom Liveblocks format that preserves information about
conflict-free data types. The easiest way to create it is using the
`toPlainLson` helper provided by `@liveblocks/client`. Note that your Storage
root should always be a `LiveObject`.

```ts highlight="11-13,18-20,25"
import { toPlainLson, LiveList, LiveObject } from "@liveblocks/client";

// Create a new room
const room = await liveblocks.createRoom("my-room-id", {
  defaultAccesses: ["room:write"],
});

// If this were your Storage type...
declare global {
  interface Liveblocks {
    Storage: {
      names: LiveList<string>;
    };
  }
}

// Create the initial conflict-free data
const initialStorage: LiveObject<Liveblocks["Storage"]> = new LiveObject({
  names: new LiveList(["Olivier", "Nimesh"]),
});

// Convert to LSON and create Storage
const storage = await liveblocks.initializeStorageDocument(
  "my-room-id",
  toPlainLson(initialStorage)
);
```

It’s also possible to create plain LSON manually, without the helper function.

```ts highlight="9-11,17-23"
// Create a new room
const room = await liveblocks.createRoom("my-room-id", {
  defaultAccesses: ["room:write"],
});

// If this were your Storage type...
declare global {
  interface Liveblocks {
    Storage: {
      names: LiveList<string>;
    };
  }
}

// Create this Storage and add names to the LiveList
const storage = await liveblocks.initializeStorageDocument("my-room-id", {
  liveblocksType: "LiveObject",
  data: {
    names: {
      liveblocksType: "LiveList",
      data: ["Olivier", "Nimesh"],
    },
  },
});
```

#### Liveblocks.mutateStorage [#mutate-storage]

Modify Storage contents from the server. No presence will be shown when you make
changes.

```ts
// Mutate a single room
await liveblocks.mutateStorage(
  "my-room-id",

  ({ root }) => {
    root.get("list").push("item3");
  }
);
```

The callback can be asynchronous, in which case a stream of mutations can happen
over time.

```ts
// Mutate a single room
await liveblocks.mutateStorage(
  "my-room-id",

  async ({ root }) => {
    // These changes happen immediately
    const animals = root.get("animals");
    animals.clear();
    animals.push("Thinking...");

    await thinkForAWhile();

    // These changes happen after `await` has run
    animals.clear();
    animals.push("🐶");
    animals.push("🦘");
  }
);
```

Learn how to
[type your Storage](/docs/api-reference/liveblocks-react#Typing-your-data).

#### Liveblocks.massMutateStorage [#mass-mutate-storage]

Modify Storage contents for multiple rooms simultaneously. With the default
query value `{}` it will loop through every room in your project.

```ts
// Mutate a number of rooms
await liveblocks.massMutateStorage(
  {},

  // Callback runs on every selected room
  ({ room, root }) => {
    // { type: "room", id: "my-room-id", metadata: {...}, ... }
    console.log(room);

    root.get("animals").push("🦍");
  }
);
```

A number of options are also available, enabling you to filter for certain
rooms. Additionally, you can set options for concurrency and provide an abort
signal to cancel the mutations.

```ts
// Mutate a number of rooms
await liveblocks.massMutateStorage(
  {
    // +++
    // Optional, filter for rooms that allow entry to group ID(s) in `groupsAccesses`
    groupIds: ["engineering", "design"],

    // Optional, filter for rooms that allow entry to a user's ID in `usersAccesses`
    userId: "my-user-id",

    // Optional, use advanced filtering
    query: {
      // Optional, filter for rooms with an ID that starts with specific string
      roomId: {
        startsWith: "liveblocks:",
      },
      // Optional, filter for rooms with custom metadata in `metadata`
      metadata: {
        roomType: "whiteboard",
      },
    },
    // +++
  },

  ({ room, root }) => {
    // { type: "room", id: "my-room-id", metadata: {...}, ... }
    console.log(room);

    root.get("animals").push("🦍");
  },

  // Optional
  // +++
  {
    concurrency: 10, // Optional, process at most 10 rooms simultaneously
    signal, // Optional, provide an abort signal to cancel mutations mid-way
  }
  // +++
);
```

Learn how to
[type your Storage](/docs/api-reference/liveblocks-react#Typing-your-data).

#### Liveblocks.deleteStorageDocument [#delete-rooms-roomId-storage]

Deletes a room’s Storage data. Calling this will disconnect all active users
from the room. Throws an error if the room isn’t found. This is a wrapper around
the
[Delete Storage Document API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-storage)
and returns no response.

```ts
await liveblocks.deleteStorageDocument("my-room-id");
```

### Yjs

#### Liveblocks.getYjsDocument [#get-rooms-roomId-ydoc]

Returns a JSON representation of a room’s Yjs document. Throws an error if the
room isn’t found. This is a wrapper around the
[Get Yjs Document API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-ydoc)
and returns the same response.

```ts
const yjsDocument = await liveblocks.getYjsDocument("my-room-id");

// { yourYText: "...", yourYArray: [...], ... }
console.log(yjsDocument);
```

A number of options are available.

```ts
const yjsDocument = await liveblocks.getYjsDocument("my-room-id", {
  // Optional, if true, `yText` values will return formatting
  format: true,

  // Optional, return a single key's value, e.g. `yDoc.get("my-key-id").toJson()`
  key: "my-key-id",

  // Optional, override the inferred `key` type, e.g. "ymap" for `doc.get(key, Y.Map)`
  type: "ymap",
});
```

#### Liveblocks.sendYjsBinaryUpdate [#put-rooms-roomId-ydoc]

Send a Yjs binary update to a room’s Yjs document. You can use this to update or
initialize the room’s Yjs document. Throws an error if the room isn’t found.
This is a wrapper around the
[Send a Binary Yjs Update API](/docs/api-reference/rest-api-endpoints#put-rooms-roomId-ydoc)
and returns no response.

```ts
await liveblocks.sendYjsBinaryUpdate("my-room-id", update);
```

Here’s an example of how to update a room’s Yjs document with your changes.

```ts
import * as Y from "yjs";

// Create a Yjs document
const yDoc = new Y.Doc();

// Create your data structures and make your update
// If you're using a text editor, you need to match its format
const yText = yDoc.getText("text");
yText.insert(0, "Hello world");

// Encode the document state as an update
const update = Y.encodeStateAsUpdate(yDoc);

// Send update to Liveblocks
await liveblocks.sendYjsBinaryUpdate("my-room-id", update);
```

To update a subdocument instead of the main document, pass its `guid`.

```ts
await liveblocks.sendYjsBinaryUpdate("my-room-id", update, {
  // Optional, update a subdocument instead. guid is its unique identifier
  guid: "c4a755...",
});
```

To create a new room and initialize its Yjs document, call
[`liveblocks.createRoom`](#post-rooms) before sending the binary update.

```ts highlight="1-2"
// Create new room
const room = await liveblocks.createRoom("my-room-id");

// Set initial Yjs document value
await liveblocks.sendYjsBinaryUpdate("my-room-id", state);
```

##### Different editors

Note that each text and code editor handles binary updates in a different way,
and may use a different Yjs shared type, for example
[`Y.XmlFragment`](https://docs.yjs.dev/api/shared-types/y.xmlfragment) instead
of [`Y.Text`](https://docs.yjs.dev/api/shared-types/y.text).

Create a binary update with [Slate](https://www.slatejs.org/):

```ts title="Slate binary update" highlight="3,13-17,19-21" isCollapsed isCollapsable
import { Liveblocks } from "@liveblocks/node";
import * as Y from "yjs";
import { slateNodesToInsertDelta } from "@slate-yjs/core";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST() {
  // Create a Yjs document
  const yDoc = new Y.Doc();

  // Create Slate document state
  const slateDoc = {
    type: "paragraph",
    children: [{ text: "Hello world" }],
  };

  // Create your data structures and make your update
  const insertDelta = slateNodesToInsertDelta(slateDoc);
  (yDoc.get("content", Y.XmlText) as Y.XmlText).applyDelta(insertDelta);

  // Encode the document state as an update
  const update = Y.encodeStateAsUpdate(yDoc);

  // Send update to Liveblocks
  await liveblocks.sendYjsBinaryUpdate("my-room-id", update);
}
```

Create a binary update with
[Tiptap](https://tiptap.dev/docs/editor/api/extensions/collaboration):

```ts title="Tiptap binary update" highlight="12-14,16-18" isCollapsed isCollapsable
import * as Y from "yjs";
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST() {
  // Create a Yjs document
  const yDoc = new Y.Doc();

  // Create Tiptap Yjs state
  const yXmlElement = new Y.XmlElement("paragraph");
  yXmlElement.insert(0, [new Y.XmlText("Hello world")]);

  // Create your data structures and make your update
  const yXmlFragment = yDoc.getXmlFragment("default");
  yXmlFragment.insert(0, [yXmlElement]);

  // Encode the document state as an update message
  const yUpdate = Y.encodeStateAsUpdate(yDoc);

  // Initialize the Yjs document with the update
  await liveblocks.sendYjsBinaryUpdate("my-room-id", {
    update: yUpdate,
  });
}
```

Read the [Yjs documentation](https://docs.yjs.dev/api/document-updates) to learn
more about creating binary updates.

#### Liveblocks.getYjsDocumentAsBinaryUpdate [#get-rooms-roomId-ydoc-binary]

Return a room’s Yjs document as a single binary update. You can use this to get
a copy of your Yjs document in your back end. Throws an error if the room isn’t
found. This is a wrapper around the
[Get Yjs Document Encoded as a Binary Yjs Update API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-ydoc-binary)
and returns the same response.

```ts
const binaryYjsUpdate =
  await liveblocks.getYjsDocumentAsBinaryUpdate("my-room-id");
```

To return a subdocument instead of the main document, pass its `guid`.

```ts
const binaryYjsUpdate = await liveblocks.getYjsDocumentAsBinaryUpdate(
  "my-room-id",
  {
    // Optional, return a subdocument instead. guid is its unique identifier
    guid: "c4a755...",
  }
);
```

Read the [Yjs documentation](https://docs.yjs.dev/api/document-updates) to learn
more about using binary updates.

### Schema validation

#### Liveblocks.createSchema [#post-create-new-schema]

Creates a schema that can be used to enforce a room’s Storage data structure.
The schema consists of a unique name, and a body which specifies the data shape
of the room in Liveblocks
[schema syntax](/docs/platform/schema-validation/syntax). This is a wrapper
around the
[Create Schema API](/docs/api-reference/rest-api-endpoints#post-create-new-schema)
and returns the same response.

```ts
const schemaBody = `
  type Storage {
    names: LiveList<string>
  }
`;

const schema = await liveblocks.createSchema("my-schema-name", schemaBody);

// { id: "my-schema-name@1", name: "my-schema-name", version: 1, ... }
console.log(schema);
```

Read the [schema validation](/docs/platform/schema-validation) page to learn
more.

#### Liveblocks.getSchema [#get-create-new-schema]

Returns a schema from its ID. A schema’s ID is a combination of its name and
version, for example the ID for version `1` of `my-schema-name` is
`my-schema-name@1`. This is a wrapper around the
[Get Schema API](/docs/api-reference/rest-api-endpoints#get-create-new-schema)
and returns the same response.

```ts
const updatedBody = `
  type Storage {
    names: LiveMap<string, string>
  }
`;

const schema = await liveblocks.getSchema("my-schema-name@1", updatedBody);

// { id: "my-schema-name@1", name: "my-schema-name", version: 1, ... }
console.log(schema);
```

Read the [schema validation](/docs/platform/schema-validation) page to learn
more.

#### Liveblocks.updateSchema [#put-update-new-schema]

Updates a schema’s body and increments its version. A schema’s body specifies
the data shape of the room in Liveblocks
[schema syntax](/docs/platform/schema-validation/syntax). Find the schema by its
ID, a combination of its name and version, for example the ID for version `1` of
`my-schema-name` is `my-schema-name@1`. This is a wrapper around the
[Update Schema API](/docs/api-reference/rest-api-endpoints#put-update-new-schema)
and returns the same response.

```ts
const schema = await liveblocks.updateSchema("my-schema-name@1");

// { id: "my-schema-name@1", name: "my-schema-name", version: 1, ... }
console.log(schema);
```

Read the [schema validation](/docs/platform/schema-validation) page to learn
more.

#### Liveblocks.deleteSchema [#delete-a-schema]

Deletes a schema. This is only allowed if the schema is not attached to a room.
Find the schema by its ID, a combination of its name and version, for example
the ID for version `1` of `my-schema-name` is `my-schema-name@1`. This is a
wrapper around the
[Delete Schema API](/docs/api-reference/rest-api-endpoints#delete-a-schema) and
returns no response.

```ts
await liveblocks.deleteSchema("my-schema-name@1");
```

Read the [schema validation](/docs/platform/schema-validation) page to learn
more.

#### Liveblocks.getSchemaByRoomId [#get-new-schema]

Returns the schema attached to a room. Throws an error if the room isn’t found.
This is a wrapper around the
[Get Schema By Room API](/docs/api-reference/rest-api-endpoints#get-new-schema)
and returns the same response.

```ts
const schema = await liveblocks.getSchemaByRoomId("my-room-id");

// { id: "my-schema-name@1", name: "my-schema-name", version: 1, ... }
console.log(schema);
```

Read the [schema validation](/docs/platform/schema-validation) page to learn
more.

#### Liveblocks.attachSchemaToRoom [#post-attach-schema-to-room]

Attaches a schema to a room, and instantly enables runtime schema validation in
it. Attach the schema by its ID, a combination of its name and version, for
example the ID for version `1` of `my-schema-name` is `my-schema-name@1`. This
is a wrapper around the
[Attach Schema to a Room API](/docs/api-reference/rest-api-endpoints#post-attach-schema-to-room)
and returns the same response.

If the current contents of the room’s Storage do not match the schema, attaching
will fail and the error message will give details on why the schema failed to
attach. It’ll also throw an error if the room isn’t found.

```ts
const schema = await liveblocks.attachSchemaToRoom(
  "my-room-id",
  "my-schema-name@1"
);

// { id: "my-schema-name@1", name: "my-schema-name", version: 1, ... }
console.log(schema);
```

Read the [schema validation](/docs/platform/schema-validation) page to learn
more.

#### Liveblocks.detachSchemaFromRoom [#delete-detach-schema-to-room]

Detaches a schema from a room. This is a wrapper around the
[Detach Schema from a Room API](/docs/api-reference/rest-api-endpoints#post-detach-schema-to-room)
and returns no response.

```ts
await liveblocks.detachSchemaFromRoom("my-room-id");
```

### Comments

#### Liveblocks.getThreads [#get-rooms-roomId-threads]

Returns a list of threads found inside a room. Throws an error if the room isn’t
found. This is a wrapper around the
[Get Room Threads API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads)
and returns the same response.

```ts
const { data: threads } = await liveblocks.getThreads({
  roomId: "my-room-id",
});

// [{ type: "thread", id: "th_d75sF3...", ... }, ...]
console.log(threads);
```

It’s also possible to filter threads by their string, boolean, and number
metadata using a query parameter. You can also pass `startsWith` to match the
start of a string.

```ts
const { data: threads } = await liveblocks.getThreads({
  roomId: "my-room-id",

  // Optional, use advanced filtering
  query: {
    // Optional, filter based on resolved status
    resolved: false,
    // Optional, filter for metadata values
    metadata: {
      status: "open",
      pinned: true,
      priority: 3,

      // You can match the start of a metadata string
      organization: {
        startsWith: "liveblocks:",
      },
    },
  },
});
```

You can also pass a
[query language](/docs/guides/how-to-filter-threads-using-query-language) string
instead of a `query` object.

#### Liveblocks.createThread [#post-rooms-roomId-threads]

Creates a new thread within a specific room, using room ID and thread data. This
is a wrapper around the
[Create Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads)
and returns the new thread.

```ts
const thread = await liveblocks.createThread({
  roomId: "my-room-id",

  data: {
    comment: {
      userId: "florent@example.com",
      body: {
        version: 1,
        content: [
          /* The comment's body text goes here, see below */
        ],
      },
    },
  },
});

// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```

A comment’s body is an array of paragraphs, each containing child nodes. Here’s
an example of how to construct a comment’s body, which can be submitted under
`data.comment.body`.

```tsx highlight="3-11,20"
import { CommentBody } from "@liveblocks/node";

const body: CommentBody = {
  version: 1,
  content: [
    {
      type: "paragraph",
      children: [{ text: "Hello " }, { text: "world", bold: true }],
    },
  ],
};

const thread = await liveblocks.createThread({
  roomId: "my-room-id",

  data: {
    // ...
    comment: {
      // The comment's body, uses the `CommentBody` type
      body,

      // ...
    },
  },
});
```

This method has a number of options, allowing for custom metadata and a creation
date for the comment.

```ts
const thread = await liveblocks.createThread({
  roomId: "my-room-id",

  data: {
    // Optional, custom metadata properties
    metadata: {
      color: "blue",
      page: 3,
      pinned: true,
    },

    // Data for the first comment in the thread
    comment: {
      // The ID of the user that created the comment
      userId: "florent@example.com",

      // Optional, when the comment was created.
      createdAt: new Date(),

      // Optional, custom comment metadata
      metadata: {
        tag: "important",
        spam: false,
      },

      // The comment's body, uses the `CommentBody` type
      body: {
        version: 1,
        content: [
          /* The comment's body text goes here, see above */
        ],
      },
    },
  },
});

// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```

#### Liveblocks.getThread [#get-rooms-roomId-threads-threadId]

Returns a thread. Throws an error if the room or thread isn’t found. This is a
wrapper around the
[Get Thread API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId)
and returns the same response.

```ts
const thread = await liveblocks.getThread({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
});

// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```

#### Liveblocks.editThreadMetadata [#post-rooms-roomId-threads-threadId-metadata]

Updates the metadata of a specific thread within a room. This method allows you
to modify the metadata of a thread, including user information and the date of
the last update. Throws an error if the room or thread isn’t found. This is a
wrapper around the
[Update Thread Metadata API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-metadata)
and returns the updated metadata.

```ts
const editedMetadata = await liveblocks.editThreadMetadata({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    metadata: {
      color: "yellow",
    },
    userId: "marc@example.com",
    updatedAt: new Date(), // Optional
  },
});

// { color: "yellow", page: 3, pinned: true }
console.log(editedMetadata);
```

Metadata can be a `string`, `number`, or `boolean`. You can also use `null` to
remove metadata from a thread. Here’s an example using every option.

```ts
const editedMetadata = await liveblocks.editThreadMetadata({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    // Custom metadata
    metadata: {
      // Metadata can be a string, number, or boolean
      title: "My thread title",
      page: 3,
      pinned: true,

      // Remove metadata with null
      color: null,
    },

    // The ID of the user that updated the metadata
    userId: "marc@example.com",

    // Optional, the time the user updated the metadata
    updatedAt: new Date(),
  },
});

// { title: "My thread title", page: 3, pinned: true }
console.log(editedMetadata);
```

#### Liveblocks.editCommentMetadata [#post-rooms-roomId-threads-threadId-comments-commentId-metadata]

Updates the metadata of a specific comment within a thread. This method allows
you to modify the metadata of a comment, including user information and the date
of the last update. Throws an error if the room, thread, or comment isn’t found.
This is a wrapper around the
[Update Comment Metadata API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-metadata)
and returns the updated metadata.

```ts
const editedMetadata = await liveblocks.editCommentMetadata({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    metadata: {
      spam: false,
    },
    userId: "stacy@example.com",
    updatedAt: new Date(), // Optional
  },
});

// { spam: false }
console.log(editedMetadata);
```

Metadata can be a `string`, `number`, or `boolean`. You can also use `null` to
remove metadata from a comment. Here’s an example using every option.

```ts
const editedMetadata = await liveblocks.editCommentMetadata({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    // Custom metadata
    metadata: {
      // Metadata can be a string, number, or boolean
      tag: "important",
      priority: 2,
      spam: true,

      // Remove metadata with null
      assignedTo: null,
    },

    // The ID of the user that updated the metadata
    userId: "stacy@example.com",

    // Optional, the time the user updated the metadata
    updatedAt: new Date(),
  },
});

// { tag: "important", priority: 2, flagged: true }
console.log(editedMetadata);
```

#### Liveblocks.markThreadAsResolved [#post-rooms-roomId-threads-threadId-mark-as-resolved]

Marks a thread as resolved, which means it sets the `resolved` property on the
specified thread to `true`. Takes a `userId`, which is the ID of the user that
resolved the thread. Throws an error if the room or thread isn’t found. This is
a wrapper around the
[Mark Thread As Resolved API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-resolved)
and returns the same response.

```ts
const thread = await liveblocks.markThreadAsResolved({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  data: {
    userId: "steven@example.com",
  },
});

// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```

#### Liveblocks.markThreadAsUnresolved [#post-rooms-roomId-threads-threadId-mark-as-unresolved]

Marks a thread as unresolved, which means it sets the `resolved` property on the
specified thread to `false`. Takes a `userId`, which is the ID of the user that
unresolved the thread. Throws an error if the room or thread isn’t found. This
is a wrapper around the
[Mark Thread As Unresolved API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-unresolved)
and returns the same response.

```ts
const thread = await liveblocks.markThreadAsUnresolved({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  data: {
    userId: "steven@example.com",
  },
});

// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```

#### Liveblocks.deleteThread [#delete-rooms-roomId-threads-threadId]

Deletes a thread. Throws an error if the room or thread isn’t found. This is a
wrapper around the
[Delete Thread API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-threads-threadId)
and returns no response.

```ts
await liveblocks.deleteThread({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
});
```

#### Liveblocks.subscribeToThread [#post-rooms-roomId-threads-threadId-subscribe]

Subscribes a user to a thread, meaning they will receive inbox notifications
when new comments are posted. Throws an error if the room or thread isn’t found.
This is a wrapper around the
[Subscribe To Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-subscribe)
and returns the same response.

```ts
const subscription = await liveblocks.subscribeToThread({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  data: {
    userId: "steven@example.com",
  },
});

// { kind: "thread", subjectId: "th_d75sF3...", ... }
console.log(subscription);
```

Subscribing will replace any existing subscription for the current thread
[set at room-level](#post-rooms-roomId-users-userId-subscription-settings). This
value can also be overridden by a room-level call that is run afterwards.

```ts
const roomId = "my-room-id";
const userId = "steven@example.com";

// 1. Disables notifications for all threads
await liveblocks.updateRoomSubscriptionSettings({
  roomId,
  userId,
  data: {
    threads: "none",
  },
});

// 2. Enables notifications just for this thread, "th_d75sF3..."
await liveblocks.subscribeToThread({
  roomId,
  threadId: "th_d75sF3...",
  data: { userId },
});

// 3. Disables notifications for all threads, including "th_d75sF3..."
await liveblocks.updateRoomSubscriptionSettings({
  roomId,
  userId,
  data: {
    threads: "none",
  },
});
```

#### Liveblocks.unsubscribeFromThread [#post-rooms-roomId-threads-threadId-unsubscribe]

Unsubscribes a user from a thread, meaning they will no longer receive inbox
notifications when new comments are posted. Throws an error if the room or
thread isn’t found. This is a wrapper around the
[Unsubscribe From Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-unsubscribe)
and returns the same response.

```ts
await liveblocks.unsubscribeFromThread({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  data: {
    userId: "steven@example.com",
  },
});
```

Unsubscribing will replace any existing subscription for the current thread
[set at room-level](#post-rooms-roomId-users-userId-subscription-settings). This
value can also be overridden by a room-level call that is run afterwards.

```ts
const roomId = "my-room-id";
const userId = "steven@example.com";

// 1. Enables notifications for all thread activity
await liveblocks.updateRoomSubscriptionSettings({
  roomId,
  userId,
  data: {
    threads: "all",
  },
});

// 2. Disables notifications just for this thread, "th_d75sF3..."
await liveblocks.unsubscribeFromThread({
  roomId,
  threadId: "th_d75sF3...",
  data: { userId },
});

// 3. Enables notifications for all thread activity, including "th_d75sF3..."
await liveblocks.updateRoomSubscriptionSettings({
  roomId,
  userId,
  data: {
    threads: "none",
  },
});
```

#### Liveblocks.getThreadSubscriptions [#get-rooms-roomId-threads-threadId-subscriptions]

Gets a thread’s subscriptions, returning a list of users that will receive
notifications when new comments are posted. Throws an error if the room or
thread isn’t found. This is a wrapper around the
[Get Thread Subscriptions API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId-subscriptions)
and returns the same response.

```ts
const { data: subscriptions } = await liveblocks.getThreadSubscriptions({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
});

// [{ kind: "thread", subjectId: "th_d75sF3...", userId: "steven@example.com", ... }, ...]
console.log(subscriptions);
```

#### Liveblocks.createComment [#post-rooms-roomId-threads-threadId-comments]

Creates a new comment in a specific thread within a room. This method allows
users to add comments to a conversation thread, specifying the user who made the
comment and the content of the comment. This method is a wrapper around the
[Create Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments)
and returns the new comment.

```ts
const comment = await liveblocks.createComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    body: {
      version: 1,
      content: [
        /* The comment's body text goes here, see below */
      ],
    },
    userId: "pierre@example.com",
    createdAt: new Date(), // Optional
  },
});
```

A comment’s body is an array of paragraphs, each containing child nodes. Here’s
an example of how to construct a comment’s body, which can be submitted under
`data.body`.

```tsx highlight="3-11,19"
import { CommentBody } from "@liveblocks/node";

const body: CommentBody = {
  version: 1,
  content: [
    {
      type: "paragraph",
      children: [{ text: "Hello " }, { text: "world", bold: true }],
    },
  ],
};

const comment = await liveblocks.createComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    // The comment's body, uses the `CommentBody` type
    body,

    // ...
  },
});
```

This method has a number of options, including the option to add a custom
creation date and metadata to the comment.

```ts
const comment = await liveblocks.createComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    // The comment's body, uses the `CommentBody` type
    body: {
      version: 1,
      content: [
        /* The comment's body text goes here, see above */
      ],
    },

    // The ID of the user that created the comment
    userId: "adrien@example.com",

    // Optional, the time the comment was created
    createdAt: new Date(),

    // Optional, custom comment metadata
    metadata: {
      tag: "important",
      reviewed: false,
    },
  },
});
```

#### Liveblocks.getComment [#get-rooms-roomId-threads-threadId-comments-commentId]

Returns a comment. Throws an error if the room, thread, or comment isn’t found.
This is a wrapper around the
[Get Comment API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId-comments-commentId)
and returns the same response.

```ts
const comment = await liveblocks.getComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",
});

// { type: "comment", threadId: "th_d75sF3...", ... }
console.log(comment);
```

#### Liveblocks.editComment [#post-rooms-roomId-threads-threadId-comments-commentId]

Edits an existing comment in a specific thread within a room. This method allows
users to update the content of their previously posted comments, with the option
to specify the time of the edit. Throws an error if the comment isn’t found.
This is a wrapper around the
[Edit Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId)
and returns the updated comment.

```ts
const editedComment = await liveblocks.editComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    userId: "alicia@example.com",
    body: {
      version: 1,
      content: [
        /* The comment's body text goes here, see below */
      ],
    },

    // Optional, the time the comment was edited
    editedAt: new Date(),

    // Optional, custom comment metadata
    metadata: {
      tag: "important",
      spam: false,
    },
  },
});

// { type: "comment", threadId: "th_d75sF3...", ... }
console.log(editedComment);
```

A comment’s body is an array of paragraphs, each containing child nodes. Here’s
an example of how to construct a comment’s body, which can be submitted under
`data.body`.

```tsx
import { CommentBody } from "@liveblocks/node";

// +++
const body: CommentBody = {
  version: 1,
  content: [
    {
      type: "paragraph",
      children: [{ text: "Hello " }, { text: "world", bold: true }],
    },
  ],
};
// +++

const editedComment = await liveblocks.editComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    // The comment's body, uses the `CommentBody` type
    // +++
    body,
    // +++

    // ...
  },
});
```

#### Liveblocks.deleteComment [#delete-rooms-roomId-threads-threadId-comments-commentId]

Deletes a specific comment from a thread within a room. If there are no
remaining comments in the thread, the thread is also deleted. This method throws
an error if the comment isn’t found. This is a wrapper around the
[Delete Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId)
and returns no response.

```ts
await liveblocks.deleteComment({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",
});
```

#### Liveblocks.addCommentReaction [#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction]

Adds a reaction to a specific comment in a thread within a room. Throws an error
if the comment isn’t found or if the user has already added the same reaction on
the comment. This is a wrapper around the
[Add Comment Reaction API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction)
and returns the new reaction.

```ts
const reaction = await liveblocks.addCommentReaction({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    emoji: "👨‍👩‍👧",
    userId: "guillaume@example.com",
    createdAt: new Date(), // Optional, the time the reaction was added
  },
});

// { emoji: "👨‍👩‍👧", userId "guillaume@example.com", ... }
console.log(reaction);
```

#### Liveblocks.removeCommentReaction [#post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction]

Removes a reaction from a specific comment in a thread within a room. Throws an
error if the comment reaction isn’t found. This is a wrapper around the
[Remove Comment Reaction API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction)
and returns no response.

```ts
await liveblocks.removeCommentReaction({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    emoji: "👨‍👩‍👧",
    userId: "steven@example.com",
    removedAt: new Date(), // Optional, the time the reaction is to be removed
  },
});
```

#### Liveblocks.getRoomSubscriptionSettings [#get-rooms-roomId-users-userId-subscription-settings]

Returns a user’s subscription settings for a specific room, specifying which
`thread` and `textMention` inbox notifications they are set to receive. This is
a wrapper around the
[Get Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-users-userId-subscription-settings).

```ts
const subscriptionSettings = await liveblocks.getRoomSubscriptionSettings({
  roomId: "my-room-id",
  userId: "steven@example.com",
});

// { threads: "all", textMentions: "mine" }
console.log(subscriptionSettings);
```

For `"threads"`, these are the three possible values:

- `"all"` Receive notifications for every activity in every thread.
- `"replies_and_mentions"` Receive notifications for mentions and threads you’re
  participating in.
- `"none"` No notifications are received.

For `"textMentions"`, these are the two possible values:

- `"mine"` Receive notifications for mentions of you.
- `"none"` No notifications are received.

#### Liveblocks.updateRoomSubscriptionSettings [#post-rooms-roomId-users-userId-subscription-settings]

Updates a user’s subscription settings for a specific room, defining which
`thread` and `textMention` inbox notifications they will receive. This is a
wrapper around the
[Update Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-users-userId-subscription-settings).

```ts
const updatedSubscriptionSettings =
  await liveblocks.updateRoomSubscriptionSettings({
    roomId: "my-room-id",
    userId: "steven@example.com",
    data: {
      threads: "replies_and_mentions",
      textMentions: "mine",
    },
  });

// { threads: "replies_and_mentions", ... }
console.log(updatedSubscriptionSettings);
```

For `"threads"`, these are the three possible values that can be set:

- `"all"` Receive notifications for every activity in every thread.
- `"replies_and_mentions"` Receive notifications for mentions and threads you’re
  participating in.
- `"none"` No notifications are received.

For `"textMentions"`, these are the two possible values that can be set:

- `"mine"` Receive notifications for mentions of you.
- `"none"` No notifications are received.

##### Replacing individual thread subscriptions

Subscribing will replace any
[existing thread subscriptions](#post-rooms-roomId-users-userId-subscription-settings)
in the current room. This value can also be overridden by a room-level call that
is run afterwards.

```ts
const roomId = "my-room-id";
const userId = "steven@example.com";

// 1. Enables notifications just for this thread, "th_d75sF3..."
await liveblocks.subscribeToThread({
  roomId,
  threadId: "th_d75sF3...",
  data: { userId },
});

// 2. Disables notifications for all threads, including "th_d75sF3..."
await liveblocks.updateRoomSubscriptionSettings({
  roomId,
  userId,
  data: {
    threads: "none",
  },
});
```

#### Liveblocks.deleteRoomSubscriptionSettings [#delete-rooms-roomId-users-userId-subscription-settings]

Deletes a user’s subscription settings for a specific room. This is a wrapper
around the
[Delete Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-users-userId-subscription-settings).

```ts
await liveblocks.deleteRoomSubscriptionSettings({
  roomId: "my-room-id",
  userId: "steven@example.com",
});
```

#### Liveblocks.getUserRoomSubscriptionSettings [#get-users-userId-room-subscription-settings]

Returns a list of a user’s subscription settings for all rooms. This is a
wrapper around the
[Get User Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#get-users-userId-room-subscription-settings).

```ts
const { data: subscriptionSettings, nextCursor } =
  await liveblocks.getUserRoomSubscriptionSettings({
    userId: "steven@example.com",

    // Optional, filter for a specific tenant
    tenantId: "acme-corp",
  });

console.log(subscriptionSettings);

// { roomId: "my-room-id", threads: "all", ... }

// Pagination
if (nextCursor) {
  const { data: nextPage } = await liveblocks.getUserRoomSubscriptionSettings({
    userId: "steven@example.com",
    startingAfter: nextCursor,
  });
}
```

### Notifications

#### Liveblocks.getInboxNotifications [#get-users-userId-inboxNotifications]

Returns a list of a user’s inbox notifications. This is a wrapper around the
[Get Inbox Notifications API](/docs/api-reference/rest-api-endpoints#get-users-userId-inboxNotifications).
It also provides an unread query parameter to filter unread notifications.

```ts
const { data: inboxNotifications, nextCursor } =
  await liveblocks.getInboxNotifications({ userId: "steven@example.com" });

// [{ id: "in_3dH7sF3...", kind: "thread", ... }, { id: "in_3dH7sF3...", kind: "textMention", ... }, ...]
console.log(inboxNotifications);

// Filter unread notifications
const { data: unreadInboxNotifications, nextCursor } =
  await liveblocks.getInboxNotifications({
    userId: "steven@example.com",
    query: { unread: true },

    // Optional, filter for a specific tenant
    tenantId: "acme-corp",
  });
```

##### Pagination

You can use `nextCursor` to paginate inbox notifications. In this example, when
`getNextPage` is called, the next page of inbox notifications is added to
`pages`.

```ts
import { InboxNotificationData } from "@liveblocks/node";

// An array of pages, each containing a list of retrieved inbox notifications
const pages: InboxNotificationData[][] = [];

// Holds the pagination cursor for the next set of inbox notifications
let startingAfter;

// Call to get the next page of inbox notifications
async function getNextPage() {
  const { data, nextCursor } = await liveblocks.getInboxNotifications({
    startingAfter,
  });
  pages.push(data);
  startingAfter = nextCursor;
}
```

If you’d like to iterate over all your inbox notifications, it’s most convenient
to use
[`liveblocks.iterInboxNotifications`](#iter-users-userId-inboxNotifications)
instead. This method automatically paginates your API requests.

#### Liveblocks.iterInboxNotifications [#iter-users-userId-inboxNotifications]

Returns a list of inbox notifications for the given user. Works similarly to
[`liveblocks.getInboxNotifications`](#get-users-userId-inboxNotifications), but
instead returns an asynchronous iterator, which helps you iterate over all the
inbox notifications, without having to manually paginate through the results.

```ts
const userId = "steven@example.com";

for await (const item of liveblocks.iterInboxNotifications({
  userId,
  // Optional, filter for a specific tenant
  tenantId: "acme-corp",
})) {
  console.log(item.id); // in_3dH7sF3...
  console.log(item.kind); // "thread", "textMention", ...
}
```

#### Liveblocks.getInboxNotification [#get-users-userId-inboxNotifications-inboxNotificationId]

Returns a user’s inbox notification. This is a wrapper around the
[Get Inbox Notification API](/docs/api-reference/rest-api-endpoints#get-users-userId-inboxNotifications-inboxNotificationId).

```ts
const inboxNotification = await liveblocks.getInboxNotification({
  userId: "steven@example.com",
  inboxNotificationId: "in_3dH7sF3...",
});

// { id: "in_3dH7sF3...", kind: "thread", ... }
// or { id: "in_3dH7sF3...", kind: "textMention", ... }
// or { id: "in_3dH7sF3...", kind: "$yourKind", ... }
console.log(inboxNotification);
```

#### Liveblocks.triggerInboxNotification [#post-inbox-notifications-trigger]

Triggers a custom inbox notification. `kind` must start with a `$`, and
represents the type of notification. `activityData` is used to send custom data
with the notification, and properties can have `string`, `number`, or `boolean`
values. Notifications [can be batched](#Batching-custom-notifications). This is
a wrapper around the
[Trigger Inbox Notification API](/docs/api-reference/rest-api-endpoints#post-inbox-notifications-trigger).

```ts
await liveblocks.triggerInboxNotification({
  // The ID of the user that will receive the inbox notification
  userId: "steven@example.com",

  // The custom notification kind, must start with a $
  kind: "$fileUploaded",

  // Custom ID for this specific notification
  subjectId: "my-file",

  // Custom data related to the activity that you need to render the inbox notification
  activityData: {
    // Data can be a string, number, or boolean
    file: "https://example.com/my-file.zip",
    size: 256,
    success: true,
  },

  // Optional, define the room ID the notification was sent from
  roomId: "my-room-id",

  // Optional, trigger it for a specific tenant
  tenantId: "acme-corp",
});
```

##### Typing custom notifications

To type custom notifications, edit the `ActivitiesData` type in your config
file.

```ts file="liveblocks.config.ts" highlight="4-10"
declare global {
  interface Liveblocks {
    // Custom activities data for custom notification kinds
    ActivitiesData: {
      // Example, a custom $alert kind
      $alert: {
        title: string;
        message: string;
      };
    };

    // Other kinds
    // ...
  }
}
```

##### Batching custom notifications

You can configure a custom notification kind to have batching enabled. When it’s
enabled, triggering an inbox notification activity for a specific `subjectId`,
will update the existing inbox notification instead of creating a new one.

To use this, you must first
[enable batching in the dashboard](/docs/ready-made-features/notifications/concepts#Notification-batching).
Next, trigger a notification with the same `subjectId` as an existing
notification, and the result will be added to the `activityData` array.

```ts
const options = {
  userId: "steven@example.com",
  kind: "$fileUploaded",
  subjectId: "my-file",
};

await liveblocks.triggerInboxNotification({
  ...options,

  // +++
  activityData: {
    status: "processing",
  },
  // +++
});

await liveblocks.triggerInboxNotification({
  ...options,

  // +++
  activityData: {
    status: "complete",
  },
  // +++
});

const { data: inboxNotifications } = await liveblocks.getInboxNotifications({
  userId: "steven@example.com",
});

// {
//   id: "in_3dH7sF3...",
//   kind: "$fileUploaded",
// +++
//   activities: [
//     { status: "processing" },
//     { status: "complete" },
//   ],
// +++
//   ...
// }
console.log(inboxNotifications[0]);
```

An inbox notification can have up to 50 activities, if you exceed this number, a
new inbox notification will be created.

#### Liveblocks.deleteInboxNotification [#delete-users-userId-inboxNotifications-inboxNotificationId]

Deletes a user’s inbox notification. This is a wrapper around the
[Delete Inbox Notification API](/docs/api-reference/rest-api-endpoints#delete-users-userId-inboxNotifications-inboxNotificationId).

```ts
await liveblocks.deleteInboxNotification({
  userId: "steven@example.com",
  inboxNotificationId: "in_3dH7sF3...",
});
```

#### Liveblocks.deleteAllInboxNotifications [#delete-users-userId-inboxNotifications]

Deletes all the user’s inbox notifications. This is a wrapper around the
[Delete Inbox Notifications API](/docs/api-reference/rest-api-endpoints#delete-users-userId-inboxNotifications).

```ts
await liveblocks.deleteAllInboxNotifications({
  userId: "steven@example.com",
  // Optional, delete for a specific tenant
  tenantId: "acme-corp",
});
```

#### Liveblocks.getNotificationSettings [#get-users-userId-notification-settings] [@badge=Beta]

Returns a user’s notification settings in the current project, in other words
which [notification webhook events](/docs/platform/webhooks#NotificationEvent)
will be sent for the user. Notification settings are project-based, which means
that this returns the user’s settings for every room. This a wrapper around the
[Get Notification Settings API](/docs/api-reference/rest-api-endpoints#get-users-userId-notification-settings).

```ts
const settings = await liveblocks.getNotificationSettings({
  userId: "guillaume@liveblocks.io",
});

// { email: { thread: true, ... }, slack: { thread: false, ... }, ... }
console.log(settings);
```

A user’s initial settings are set in the dashboard, and different kinds should
be enabled there. If no kind is enabled on the current channel, `null` will be
returned. For example, with the email channel:

```ts
const settings = await liveblocks.getNotificationSettings({
  userId: "guillaume@liveblocks.io",
});

// { email: null, ... }
console.log(settings);
```

#### Liveblocks.updateNotificationSettings [#post-users-userId-notification-settings] [@badge=Beta]

Updates a user’s notification settings, which affects which
[notification webhook events](/docs/platform/webhooks#NotificationEvent) will be
sent for the user. Notification settings are project-based, which means that
this modifies the user’s settings in every room. Each notification `kind` must
first be enabled on your project’s notification dashboard page before settings
can be used. This a wrapper around the
[Update Notification Settings API](/docs/api-reference/rest-api-endpoints#post-users-userId-notification-settings).

```ts
const updatedSettings = await liveblocks.updateNotificationSettings({
  userId: "steven@example.com",
  data: {
    email: { thread: false },
    slack: { textMention: true },
  },
});

// { email: { thread: false, ... }, slack: { textMention: true, ... }, ... }
console.log(updatedSettings);
```

You can pass a partial object, or many settings at once.

```ts
// You only need to pass partials
await liveblocks.updateNotificationSettings({
  userId: "steven@example.com",
  email: { thread: true },
});

// Enabling a custom notification on the slack channel
await liveblocks.updateNotificationSettings({
  userId: "steven@example.com",
  slack: { $myCustomNotification: true },
});

// Setting complex settings
await liveblocks.updateNotificationSettings({
  userId: "steven@example.com",
  email: {
    thread: true,
    textMention: false,
    $newDocument: true,
  },
  slack: {
    thread: false,
    $fileUpload: false,
  },
  teams: {
    thread: true,
  },
});
```

#### Liveblocks.deleteNotificationSettings [#delete-users-userId-notification-settings] [@badge=Beta]

Deletes the user’s notification settings, resetting them to the default values.
The default values can be adjusted in a project’s notification dashboard page.
This a wrapper around the
[Delete Notification Settings API](/docs/api-reference/rest-api-endpoints#delete-users-userId-notification-settings).

```ts
await liveblocks.deleteNotificationSettings({
  userId: "adri@example.com",
});
```

### AI Copilots

#### Liveblocks.getAiCopilots [#get-ai-copilots]

Returns a paginated list of AI copilots. The copilots are returned sorted by
creation date, from newest to oldest. This is a wrapper around the
[Get AI Copilots API](/docs/api-reference/rest-api-endpoints#get-ai-copilots)
and returns the same response.

```ts
const { data: copilots, nextCursor } = await liveblocks.getAiCopilots();

// A list of AI copilots
// [{ type: "copilot", id: "co_abc123...", name: "My Copilot", ... }, ...]
console.log(copilots);

// A pagination cursor used for retrieving the next page of results with `startingAfter`
// "L3YyL3Jvb21z..."
console.log(nextCursor);
```

Pagination options are available to control the number of results returned.

```ts
const { data: copilots, nextCursor } = await liveblocks.getAiCopilots({
  // Optional, the amount of copilots to load, between 1 and 100, defaults to 20
  limit: 20,

  // Optional, cursor used for pagination, use `nextCursor` from the previous page's response
  startingAfter: "L3YyL3Jvb21z...",
});
```

#### Liveblocks.createAiCopilot [#create-ai-copilot]

Creates a new AI copilot with the given configuration. This is a wrapper around
the
[Create AI Copilot API](/docs/api-reference/rest-api-endpoints#create-ai-copilot)
and returns the same response.

```ts
const copilot = await liveblocks.createAiCopilot({
  name: "My AI Assistant",
  systemPrompt: "You are a helpful AI assistant for our team.",
  provider: "openai",
  providerModel: "gpt-4",
  providerApiKey: "sk-...", // Your OpenAI API key
});

// { type: "copilot", id: "co_abc123...", name: "My AI Assistant", ... }
console.log(copilot);
```

The method supports various configuration options for different AI providers.

```ts
const copilot = await liveblocks.createAiCopilot({
  // Required, the name of the copilot
  name: "Documentation Helper",

  // Optional, a description of what the copilot does
  description: "Helps users understand our documentation",

  // Required, the system prompt that defines the copilot's behavior
  systemPrompt:
    "You are an expert at helping users understand technical documentation.",

  // Optional, additional knowledge context for the copilot
  knowledgePrompt: "Use our company's style guide when providing examples.",

  // Required, the AI provider to use
  provider: "openai",

  // Required for standard providers, the model to use
  providerModel: "gpt-4-turbo",

  // Required, your API key for the provider
  providerApiKey: "sk-...",

  // Optional, provider-specific options
  providerOptions: {
    openai: {
      reasoningEffort: "low",
      // Optional, restrict web search to specific domains for OpenAI
      webSearch: {
        allowedDomains: ["docs.liveblocks.io", "example.com"],
      },
    },
  },

  // Optional, model settings
  settings: {
    maxTokens: 1000,
    temperature: 0.7,
    topP: 0.9,
    frequencyPenalty: 0.1,
    presencePenalty: 0.1,
    stopSequences: ["END"],
    seed: 42,
    maxRetries: 3,
  },
});
```

For OpenAI-compatible providers, use a different configuration:

```ts
const copilot = await liveblocks.createAiCopilot({
  name: "Custom AI Helper",
  systemPrompt: "You are a helpful assistant.",
  provider: "openai-compatible",
  compatibleProviderName: "my-custom-provider",
  providerBaseUrl: "https://api.mycustomprovider.com/v1",
  providerApiKey: "your-api-key-here", // Your API key for the custom provider
  providerModel: "custom-provider-model",
});
```

You can also configure Anthropic or Google providers with provider-specific
options:

```ts
// Anthropic example
await liveblocks.createAiCopilot({
  name: "Anthropic Helper",
  systemPrompt: "You are a helpful assistant.",
  provider: "anthropic",
  providerModel: "claude-3-5-sonnet-latest",
  providerApiKey: "sk-...",
  providerOptions: {
    anthropic: {
      thinking: { type: "disabled" },
      webSearch: {
        allowedDomains: ["example.com"],
      },
    },
  },
});

// Google example
await liveblocks.createAiCopilot({
  name: "Gemini Helper",
  systemPrompt: "You are a helpful assistant.",
  provider: "google",
  providerModel: "gemini-2.5-pro",
  providerApiKey: "sk-...",
  providerOptions: {
    google: {
      thinkingConfig: { thinkingBudget: 2000 },
    },
  },
});
```

#### Liveblocks.getAiCopilot [#get-ai-copilot]

Returns an AI copilot by its ID. Throws an error if the copilot isn’t found.
This is a wrapper around the
[Get AI Copilot API](/docs/api-reference/rest-api-endpoints#get-ai-copilot) and
returns the same response.

```ts
const copilot = await liveblocks.getAiCopilot("co_abc123...");

// { type: "copilot", id: "co_abc123...", name: "My AI Assistant", ... }
console.log(copilot);
```

#### Liveblocks.updateAiCopilot [#update-ai-copilot]

Updates an existing AI copilot’s configuration. You only need to pass the
properties you want to update. Throws an error if the copilot isn’t found. This
is a wrapper around the
[Update AI Copilot API](/docs/api-reference/rest-api-endpoints#update-ai-copilot)
and returns the same response.

```ts
const updatedCopilot = await liveblocks.updateAiCopilot("co_abc123...", {
  name: "Updated AI Assistant",
  description: "Now with improved capabilities",
});

// { type: "copilot", id: "co_abc123...", name: "Updated AI Assistant", ... }
console.log(updatedCopilot);
```

You can update various aspects of the copilot:

```ts
const updatedCopilot = await liveblocks.updateAiCopilot("co_abc123...", {
  // Optional, update the name
  name: "Better AI Helper",

  // Optional, update the description
  description: "Enhanced with new features",

  // Optional, update the system prompt
  systemPrompt: "You are an even more helpful AI assistant.",

  // Optional, update the knowledge prompt
  knowledgePrompt: "Reference our latest guidelines.",

  // Optional, update provider-specific options (replaces the entire nested options object)
  // Set to null to clear options
  providerOptions: null,

  // Optional, update model settings
  settings: {
    temperature: 0.5,
    maxTokens: 1500,
  },
});
```

You can also update provider options. When updating `providerOptions`, it fully
replaces the previous nested options (no deep merge):

```ts
// Update OpenAI provider options (replaces prior options)
await liveblocks.updateAiCopilot("co_abc123...", {
  provider: "openai",
  providerOptions: {
    openai: {
      reasoningEffort: "medium",
      webSearch: { allowedDomains: ["docs.liveblocks.io"] },
    },
  },
});

// Update Anthropic thinking/web search settings
await liveblocks.updateAiCopilot("co_abc123...", {
  provider: "anthropic",
  providerOptions: {
    anthropic: {
      thinking: { type: "enabled", budgetTokens: 2000 },
      webSearch: { maxUses: 2 },
    },
  },
});
```

Certain properties can be set to `null` to clear them from the copilot’s
configuration. This includes `description`, `knowledgePrompt`, `settings`, and
`providerOptions`.

```ts
const updatedCopilot = await liveblocks.updateAiCopilot("co_abc123...", {
  // Clear the description
  description: null,

  // Clear the knowledge prompt
  knowledgePrompt: null,

  // Clear all model settings
  settings: null,
});
```

The method returns a 422 response if the update doesn’t apply due to validation
failures. For example, if the existing copilot uses the "openai" provider and
you attempt to update the provider model to an incompatible value for the
provider, like "gemini-2.5-pro", you’ll receive a 422 response with an error
message explaining where the validation failed.

#### Liveblocks.deleteAiCopilot [#delete-ai-copilot]

Deletes an AI copilot by its ID. A deleted copilot is no longer accessible and
cannot be restored. Throws an error if the copilot isn’t found. This is a
wrapper around the
[Delete AI Copilot API](/docs/api-reference/rest-api-endpoints#delete-ai-copilot)
and returns no response.

```ts
await liveblocks.deleteAiCopilot("co_abc123...");
```

### Knowledge Sources

#### Liveblocks.createWebKnowledgeSource [#create-web-knowledge-source]

Creates a web knowledge source for an AI copilot. This allows the copilot to
access and learn from web content. This is a wrapper around the
[Create Web Knowledge Source API](/docs/api-reference/rest-api-endpoints#create-web-knowledge-source)
and returns the ID of the created knowledge source.

```ts
const { id } = await liveblocks.createWebKnowledgeSource({
  copilotId: "co_abc123...",
  url: "https://example.com/documentation",
  type: "individual_link",
});

// "ks_def456..."
console.log(id);
```

Different types of web knowledge sources are supported:

```ts
// Index a single web page
const singlePage = await liveblocks.createWebKnowledgeSource({
  copilotId: "co_abc123...",
  url: "https://example.com/important-page",
  type: "individual_link",
});

// Crawl an entire website
const crawledSite = await liveblocks.createWebKnowledgeSource({
  copilotId: "co_abc123...",
  url: "https://example.com",
  type: "crawl",
});

// Use a sitemap to index multiple pages
const sitemapSource = await liveblocks.createWebKnowledgeSource({
  copilotId: "co_abc123...",
  url: "https://example.com/sitemap.xml",
  type: "sitemap",
});
```

#### Liveblocks.createFileKnowledgeSource [#create-file-knowledge-source]

Creates a file knowledge source for an AI copilot by uploading a file. The
copilot can then reference the content of the file when responding. This is a
wrapper around the
[Create File Knowledge Source API](/docs/api-reference/rest-api-endpoints#create-file-knowledge-source)
and returns the ID of the created knowledge source.

**Note:** Currently only PDF files (`application/pdf`) and images (`image/*`)
are supported.

```ts
const { id } = await liveblocks.createFileKnowledgeSource({
  copilotId: "co_abc123...",
  file: pdfFile, // Must be a PDF or image file
});

// "ks_ghi789..."
console.log(id);
```

#### Liveblocks.getKnowledgeSources [#get-knowledge-sources]

Returns a paginated list of knowledge sources for a specific AI copilot. This is
a wrapper around the
[Get Knowledge Sources API](/docs/api-reference/rest-api-endpoints#get-knowledge-sources)
and returns the same response.

```ts
const { data: sources, nextCursor } = await liveblocks.getKnowledgeSources({
  copilotId: "co_abc123...",
});

// [{ type: "ai-knowledge-web-source", id: "ks_abc123...", ... }, ...]
console.log(sources);
```

Pagination options are available:

```ts
const { data: sources, nextCursor } = await liveblocks.getKnowledgeSources({
  copilotId: "co_abc123...",
  // Optional, the amount of knowledge sources to load, between 1 and 100, defaults to 20
  limit: 20,
  // Optional, cursor used for pagination
  startingAfter: "L3YyL3Jvb21z...",
});
```

#### Liveblocks.getKnowledgeSource [#get-knowledge-source]

Returns a specific knowledge source by its ID. Throws an error if the knowledge
source isn’t found. This is a wrapper around the
[Get Knowledge Source API](/docs/api-reference/rest-api-endpoints#get-knowledge-source)
and returns the same response.

```ts
const source = await liveblocks.getKnowledgeSource({
  copilotId: "co_abc123...",
  knowledgeSourceId: "ks_def456...",
});

// { type: "ai-knowledge-web-source", id: "ks_def456...", ... }
// or { type: "ai-knowledge-file-source", id: "ks_def456...", ... }
console.log(source);
```

#### Liveblocks.getFileKnowledgeSourceMarkdown [#get-file-knowledge-source-markdown]

Returns the content of a file knowledge source as Markdown. This allows you to
see what content the AI copilot has access to from uploaded files. Throws an
error if the knowledge source isn’t found. This is a wrapper around the
[Get File Knowledge Source Content API](/docs/api-reference/rest-api-endpoints#get-file-knowledge-source-content)
and returns the content as a string.

```ts
const content = await liveblocks.getFileKnowledgeSourceMarkdown({
  copilotId: "co_abc123...",
  knowledgeSourceId: "ks_def456...",
});

// "# Document Title\n\nThis is the content of the uploaded file..."
console.log(content);
```

#### Liveblocks.getWebKnowledgeSourceLinks [#get-web-knowledge-source-links]

Returns a paginated list of links that were indexed from a web knowledge source.
This is useful for understanding what content the AI copilot has access to from
web sources. This is a wrapper around the
[Get Web Knowledge Source Links API](/docs/api-reference/rest-api-endpoints#get-web-knowledge-source-links)
and returns the same response.

```ts
const { data: links, nextCursor } = await liveblocks.getWebKnowledgeSourceLinks(
  {
    copilotId: "co_abc123...",
    knowledgeSourceId: "ks_def456...",
  }
);

// [{ id: "link_123...", url: "https://example.com/page1", status: "ready", ... }, ...]
console.log(links);
```

Pagination options are available:

```ts
const { data: links, nextCursor } = await liveblocks.getWebKnowledgeSourceLinks(
  {
    copilotId: "co_abc123...",
    knowledgeSourceId: "ks_def456...",
    // Optional, the amount of links to load, between 1 and 100, defaults to 20
    limit: 20,
    // Optional, cursor used for pagination
    startingAfter: "L3YyL3Jvb21z...",
  }
);
```

#### Liveblocks.deleteWebKnowledgeSource [#delete-web-knowledge-source]

Deletes a web knowledge source from an AI copilot. The copilot will no longer
have access to the content from this source. Throws an error if the knowledge
source isn’t found. This is a wrapper around the
[Delete Web Knowledge Source API](/docs/api-reference/rest-api-endpoints#delete-web-knowledge-source)
and returns no response.

```ts
await liveblocks.deleteWebKnowledgeSource({
  copilotId: "co_abc123...",
  knowledgeSourceId: "ks_def456...",
});
```

#### Liveblocks.deleteFileKnowledgeSource [#delete-file-knowledge-source]

Deletes a file knowledge source from an AI copilot. The copilot will no longer
have access to the content from this file. Throws an error if the knowledge
source isn’t found. This is a wrapper around the
[Delete File Knowledge Source API](/docs/api-reference/rest-api-endpoints#delete-file-knowledge-source)
and returns no response.

```ts
await liveblocks.deleteFileKnowledgeSource({
  copilotId: "co_abc123...",
  knowledgeSourceId: "ks_def456...",
});
```

### Error handling [#error-handling]

Errors in our API methods, such as network failures, invalid arguments, or
server-side issues, are reported through the `LiveblocksError` class. This
custom error class extends the standard JavaScript
[`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)
and includes a `status` property, which provides the HTTP status code for the
error, such as 404 for not found or 500 for server errors.

Example of handling errors in a typical API call:

```ts
try {
  const room = await liveblocks.getRoom("my-room-id");
  // Process room
} catch (error) {
  if (error instanceof LiveblocksError) {
    // Handle specific LiveblocksError cases
    console.error(`Error fetching room: ${error.status} - ${error.message}`);
    switch (
      error.status
      // Specific cases based on status codes
    ) {
    }
  } else {
    // Handle general errors
    console.error(`Unexpected error: ${error.message}`);
  }
}
```

## Utilities

### getMentionsFromCommentBody [#get-mentions-from-comment-body]

Returns an array of mentions from a `CommentBody` (found under `comment.body`).

```ts
import { getMentionsFromCommentBody } from "@liveblocks/node";

const mentions = getMentionsFromCommentBody(comment.body);
```

An optional second argument can be used to filter the returned mentions. By
default, if it’s not provided, all mentions are returned, including future
mention kinds (e.g. group mentions in the future).

```tsx
// All mentions (same as `getMentionsFromCommentBody(commentBody)`)
getMentionsFromCommentBody(commentBody);

// Only user mentions with an ID of "123"
getMentionsFromCommentBody(
  commentBody,
  (mention) => mention.kind === "user" && mention.id === "123"
);

// Only mentions with an ID which starts with "prefix:"
getMentionsFromCommentBody(commentBody, (mention) => (
  mention.id.startsWith("prefix:")
);
```

This is most commonly used in combination with the
[Comments API functions](/docs/api-reference/liveblocks-node#Comments), for
example [`getComment`](/docs/api-reference/liveblocks-node#get-comment).

```ts
import { Liveblocks, getMentionsFromCommentBody } from "@liveblocks/node";

// Create a node client
const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

// Retrieve a comment
const comment = await liveblocks.getComment({
  roomId: "my-room-id",
  threadId: "my-thread-id",
  commentId: "my-comment-id",
});

// Get the mentions inside the comment's body
const mentions = getMentionsFromCommentBody(comment.body);

// [{ kind: "user", id: "marc@example.com" }, { kind: "user", id: "vincent@example.com" }, ...]
console.log(mentions);
```

Here’s an example with a custom `CommentBody`.

```ts
import { CommentBody, getMentionsFromCommentBody } from "@liveblocks/node";

// Create a custom `CommentBody`
const commentBody: CommentBody = {
  version: 1,
  content: [
    {
      type: "paragraph",
      children: [
        { text: "Hello " },
        { type: "mention", id: "chris@example.com" },
      ],
    },
  ],
};

// Get the mentions inside the comment's body
const mentions = getMentionsFromCommentBody(commentBody);

// [{ kind: "user", id: "chris@example.com" }]
console.log(mentions);
```

<Banner title="Also available from @liveblocks/client">

If you’d like to use this on the client side, it’s also available from
[`@liveblocks/client`](/docs/api-reference/liveblocks-client#get-mentions-from-comment-body).

</Banner>

### stringifyCommentBody [#stringify-comment-body]

Used to convert a `CommentBody` (found under `comment.body`) into either a plain
string, Markdown, HTML, or a custom format.

```ts
import { stringifyCommentBody } from "@liveblocks/node";

const stringComment = await stringifyCommentBody(comment.body);
```

This is most commonly used in combination with the
[Comments API functions](/docs/api-reference/liveblocks-node#Comments), for
example [`getComment`](/docs/api-reference/liveblocks-node#get-comment).

```ts
import { Liveblocks, stringifyCommentBody } from "@liveblocks/node";

// Create a node client
const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

// Retrieve a comment
const comment = await liveblocks.getComment({
  roomId: "my-room-id",
  threadId: "my-thread-id",
  commentId: "my-comment-id",
});

// Convert CommentBody to plain string
const stringComment = await stringifyCommentBody(comment.body);

// "Hello marc@example.com from https://liveblocks.io"
console.log(stringComment);
```

A number of options are also available.

```ts
import { stringifyCommentBody } from "@liveblocks/client";

const stringComment = await stringifyCommentBody(comment.body, {
  // Optional, convert to specific format, "plain" (default) | "markdown" | "html"
  format: "markdown",

  // Optional, supply a separator to be used between paragraphs
  separator: `\n\n`,

  // Optional, override any elements in the CommentBody with a custom string
  elements: {
    // Optional, override the `paragraph` element
    paragraph: ({ element, children }) => `<p>${children}</p>`,

    // Optional, override the `text` element
    text: ({ element }) =>
      element.bold ? `<strong>${element.text}</strong>` : `${element.text}`,

    // Optional, override the `link` element
    link: ({ element, href }) =>
      `<a href="${href}" target="_blank">${element.url}</a>`,

    // Optional, override the `mention` element.
    // `user` and `group` are the optional data returned from `resolveUsers` and `resolveGroupsInfo`
    mention: ({ element, user, group }) =>
      `<a href="${user?.profileUrl ?? group?.settingsUrl ?? "#"}">${element.id}</a>`,
  },

  // Optional, get your user’s names and info from their ID to be displayed in mentions
  async resolveUsers({ userIds }) {
    const usersData = await __getUsersFromDB__(userIds);

    return usersData.map((userData) => ({
      // Name is inserted into the output instead of a user’s ID
      name: userData.name,

      // Custom formatting in `elements.mention` allows custom properties to be used
      profileUrl: userData.profileUrl,
    }));
  },

  // Optional, get your group’s names and info from their ID to be displayed in mentions
  async resolveGroupsInfo({ groupIds }) {
    const groupsData = await __getGroupsFromDB__(groupIds);

    return groupsData.map((groupData) => ({
      // Name is inserted into the output instead of a group’s ID
      name: groupData.name,

      // Custom formatting in `elements.mention` allows custom properties to be used
      settingsUrl: groupData.settingsUrl,
    }));
  },
});
```

<Banner title="Also available from @liveblocks/client">

If you’d like to use this on the client side, it’s also available from
[`@liveblocks/client`](/docs/api-reference/liveblocks-client#stringify-comment-body).

</Banner>

#### Formatting examples

Here are a number of different formatting examples derived from the same
`CommentBody`.

```ts
// "Hello marc@example.com from https://liveblocks.io"
await stringifyCommentBody(comment.body);

// "Hello @Marc from https://liveblocks.io"
await stringifyCommentBody(comment.body, {
  resolveUsers({ userIds }) {
    return [{ name: "Marc" }];
  },
});

// "**Hello** @Marc from [https://liveblocks.io](https://liveblocks.io)"
await stringifyCommentBody(comment.body, {
  format: "markdown",

  resolveUsers() {
    return [{ name: "Marc" }];
  },
});

// "<b>Hello</b> <span data-mention>@Marc</span> from
// <a href="https://liveblocks.io">https://liveblocks.io</a>"
await stringifyCommentBody(comment.body, {
  format: "html",

  resolveUsers() {
    return [{ name: "Marc" }];
  },
});

// "<b>Hello</b> <a href="https://example.com" data-id="marc@example.com">@Marc</a> from
// <a href="https://liveblocks.io">https://liveblocks.io</a>"
await stringifyCommentBody(comment.body, {
  format: "html",

  mention: ({ element, user }) =>
    `<a href="${user.profileUrl}" data-id="${element.id}">${user.name}</a>`,

  resolveUsers() {
    return [{ name: "Marc", profileUrl: "https://example.com" }];
  },
});
```

### WebhookHandler [#WebhookHandler]

<Banner title="Need help implementing webhooks?">

Read the [Webhooks guide](/docs/platform/webhooks) to learn how to use them
within your product, allowing you to react to Liveblocks events as they happen.

</Banner>

The `WebhookHandler` class is a helper to handle webhook requests from
Liveblocks.

It’s initialized with a signing secret that you can find in your project’s
webhook page.

```js
const webhookHandler = new WebhookHandler(process.env.WEBHOOK_SECRET);
```

#### verifyRequest [#verifyRequest]

Verifies the request and returns the event. Note that `rawBody` takes the body
as a `string`.

```js
const event = webhookHandler.verifyRequest({
  headers: req.headers,
  rawBody: req.body,
});
```

Some frameworks parse request bodies into objects, which means using
`JSON.stringify` may be necessary.

```js highlight="3"
const event = webhookHandler.verifyRequest({
  headers: req.headers,
  rawBody: JSON.stringify(req.body),
});
```

##### Example using Next.js [#webhook-example]

```js
import { WebhookHandler } from "@liveblocks/node";

// Will fail if not properly initialized with a secret
// Obtained from the Webhooks section of your project dashboard
// https://liveblocks.io/dashboard
const webhookHandler = new WebhookHandler(process.env.WEBHOOK_SECRET);

export function POST(request) {
  try {
    const event = webhookHandler.verifyRequest({
      headers: req.headers,
      rawBody: JSON.stringify(req.body),
    });

    // Handle `WebhookEvent`

    if (event.type === "storageUpdated") {
      // Handle `StorageUpdatedEvent`
    } else if (event.type === "userEntered") {
      // Handle `UserEnteredEvent`
    } else if (event.type === "userLeft") {
      // Handle `UserLeftEvent`
    }
  } catch (error) {
    console.error(error);
    return new Response(error, { status: 400 });
  }
}
```

### isThreadNotificationEvent [#isThreadNotificationEvent]

Type guard to check if a received webhook event is a
[`ThreadNotificationEvent`](/docs/platform/webhooks#Thread-notification) send
from Comments. Particularly helpful when creating
[thread notification emails](/docs/api-reference/liveblocks-emails#thread-notification-emails)
with webhooks.

```js
import { isThreadNotificationEvent } from "@liveblocks/node";

const event = webhookHandler.verifyRequest({
  headers: req.headers,
  rawBody: req.body,
});

// +++
if (isThreadNotificationEvent(event)) {
  // Handle `ThreadNotificationEvent`
}
// +++
```

The check is made against the event type and event data kind.

### isTextMentionNotificationEvent [#isTextMentionNotificationEvent]

Type guard to check if a received webhook event is a
[`TextMentionNotificationEvent`](/docs/platform/webhooks#TextMention-notification)
sent from Text Editor. Particularly helpful for identifying text mentions when
sending email notifications.

```js
import { isTextMentionNotificationEvent } from "@liveblocks/node";

const event = webhookHandler.verifyRequest({
  headers: req.headers,
  rawBody: req.body,
});

// +++
if (isTextMentionNotificationEvent(event)) {
  // Handle `TextMentionNotificationEvent`
}
// +++
```

### isCustomNotificationEvent [#isCustomNotificationEvent]

Type guard to check if a received webhook event is a
[`CustomNotificationEvent`](/docs/platform/webhooks#Custom-notification) sent
from
[`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger).
Particularly helpful for identifying custom notifications when sending email
notifications.

```js
import { isCustomNotificationEvent } from "@liveblocks/node";

const event = webhookHandler.verifyRequest({
  headers: req.headers,
  rawBody: req.body,
});

// +++
if (isCustomNotificationEvent(event)) {
  // Handle `CustomNotificationEvent`
}
// +++
```

The check is made against the event type and event data kind.

[`room.getothers`]: /docs/api-reference/liveblocks-client#Room.getOthers
[Permissions REST API]: /docs/authentication/id-token

---
meta:
  title: "@liveblocks/react-blocknote"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/react-blocknote package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/react-blocknote` provides you with a [React](https://react.dev/)
plugin that adds collaboration to any
[BlockNote rich-text editor](https://www.blocknotejs.org/). It also adds
realtime cursors, document persistence on the cloud, comments, and mentions. Use
[`@liveblocks/node-prosemirror`](/docs/api-reference/liveblocks-node-prosemirror)
for server-side editing.

## Setup

To set up your collaborative BlockNote editor, create an editor with
[`useCreateBlockNoteWithLiveblocks`](#useCreateBlockNoteWithLiveblocks) and pass
it into the
[`BlockNoteView`](https://www.blocknotejs.org/docs/editor-basics/setup#rendering-the-editor-with-blocknoteview)
component provided by `@blocknote/mantine`.

```tsx
import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";

function TextEditor() {
  // +++
  const editor = useCreateBlockNoteWithLiveblocks({});
  // +++

  return (
    <div>
      // +++
      <BlockNoteView editor={editor} />
      // +++
    </div>
  );
}
```

Liveblocks BlockNote components should be passed `editor` to enable them.

```tsx
import {
  useCreateBlockNoteWithLiveblocks,
  // +++
  FloatingComposer,
  // +++
} from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";

function TextEditor() {
  const editor = useCreateBlockNoteWithLiveblocks({});

  return (
    <div>
      <BlockNoteView editor={editor} />
      // +++
      <FloatingComposer editor={editor} style={{ width: "350px" }} />
      // +++
    </div>
  );
}
```

Learn more in our [get started guides](/docs/get-started/text-editor/blocknote).

## Default components

### FloatingComposer

Displays a [`Composer`][] near the current BlockNote selection, allowing you to
create threads.

```tsx highlight="3"
<FloatingComposer editor={editor} />
```

<Figure>
  <Image
    src="/assets/text-editor/floating-composer.jpg"
    alt="FloatingComposer"
    width={960}
    height={558}
  />
</Figure>

Submitting a comment will attach an annotation thread at the current selection.
Should be passed your BlockNote `editor`, and it’s recommended you set a width
value. Display created threads with [`AnchoredThreads`][] or
[`FloatingThreads`][].

```tsx
import {
  // +++
  FloatingComposer,
  // +++
  FloatingThreads,
  useCreateBlockNoteWithLiveblocks,
} from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";

function TextEditor() {
  const editor = useCreateBlockNoteWithLiveblocks({});

  return (
    <div>
      <BlockNoteView editor={editor} />
      // +++
      <FloatingComposer editor={editor} style={{ width: "350px" }} />
      // +++
      <FloatingThreads editor={editor} style={{ width: "350px" }} />
    </div>
  );
}
```

#### Opening the composer

To open the `FloatingComposer`, you need to click the comment button in the
BlockNote toolbar, or call the `addPendingComment`
[command](https://tiptap.dev/docs/editor/api/commands) added by Liveblocks. You
can use `liveblocksCommentMark` to check if the current selection is a comment.

```tsx
import { BlockNoteEditor } from "@blocknote/core";

function Toolbar({ editor }: { editor: BlockNoteEditor | null }) {
  if (!editor) {
    return null;
  }

  return (
    <button
      onClick={() => {
        // +++
        (editor._tiptapEditor as any).chain().focus().addPendingComment().run();
        // +++
      }}
      // +++
      data-active={(editor._tiptapEditor as any).isActive(
        "liveblocksCommentMark"
      )}
      // +++
    >
      💬 New comment
    </button>
  );
}
```

#### Props [#FloatingComposer-props]

<PropertiesList>
  <PropertiesListItem name="metadata" type="ThreadMetadata">
    The metadata of the thread to create.
  </PropertiesListItem>
  <PropertiesListItem name="commentMetadata" type="CommentMetadata">
    The metadata of the comment to create.
  </PropertiesListItem>
  <PropertiesListItem
    name="onComposerSubmit"
    type="function"
    detailedType="(comment: ComposerSubmitComment, event: FormEvent<HTMLFormElement>) => Promise<void> | void"
  >
    The event handler called when the composer is submitted.
  </PropertiesListItem>
  <PropertiesListItem name="defaultValue" type="CommentBody">
    The composer’s initial value.
  </PropertiesListItem>
  <PropertiesListItem name="collapsed" type="boolean">
    Whether the composer is collapsed. Setting a value will make the composer
    controlled.
  </PropertiesListItem>
  <PropertiesListItem
    name="onCollapsedChange"
    type="function"
    detailedType="(collapsed: boolean) => void"
  >
    The event handler called when the collapsed state of the composer changes.
  </PropertiesListItem>
  <PropertiesListItem name="defaultCollapsed" type="boolean">
    Whether the composer is initially collapsed. Setting a value will make the
    composer uncontrolled.
  </PropertiesListItem>
  <PropertiesListItem name="disabled" type="boolean">
    Whether the composer is disabled.
  </PropertiesListItem>
  <PropertiesListItem name="autoFocus" type="boolean">
    Whether to focus the composer on mount.
  </PropertiesListItem>
  <PropertiesListItem
    name="overrides"
    type="Partial<GlobalOverrides & ComposerOverrides>"
  >
    Override the component’s strings.
  </PropertiesListItem>
</PropertiesList>

### FloatingThreads

Displays floating [`Thread`][] components below text highlights in the editor.

```tsx highlight="3"
<FloatingThreads editor={editor} threads={threads} />
```

<Figure>
  <Image
    src="/assets/text-editor/floating-threads.jpg"
    alt="FloatingThreads"
    width={960}
    height={558}
  />
</Figure>

Takes a list of threads retrieved from [`useThreads`][] and renders them to the
page. Each thread is opened by clicking on its corresponding text highlight.
Should be passed your BlockNote `editor`, and it’s recommended you set a width
value.

```tsx
// +++
import { useThreads } from "@liveblocks/react/suspense";
// +++
import {
  FloatingComposer,
  // +++
  FloatingThreads,
  // +++
  useCreateBlockNoteWithLiveblocks,
} from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";

function TextEditor() {
  const editor = useCreateBlockNoteWithLiveblocks({});

  // +++
  const { threads } = useThreads();
  // +++

  return (
    <div>
      <BlockNoteView editor={editor} />
      <FloatingComposer editor={editor} style={{ width: "350px" }} />
      // +++
      <FloatingThreads
        editor={editor}
        threads={threads}
        style={{ width: "350px" }}
      />
      // +++
    </div>
  );
}
```

<Banner title="Resolved threads">

The `FloatingThreads` component automatically excludes resolved threads from
display. Any resolved threads passed in the threads list will not be shown.

</Banner>

#### Recommended usage [#FloatingThreads-recommended-usage]

[`FloatingThreads`][] and [`AnchoredThreads`][] have been designed to work
together to provide the optimal experience on mobile and desktop. We generally
recommend using both components, hiding one on smaller screens, as we are below
with Tailwind classes. Most apps also don’t need to display resolved threads, so
we can filter those out with a [`useThreads`][] option.

```tsx
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-blocknote";
import { BlockNoteEditor } from "@blocknote/core";

function ThreadOverlay({ editor }: { editor: BlockNoteEditor | null }) {
  const { threads } = useThreads({ query: { resolved: false } });

  return (
    <>
      <FloatingThreads
        editor={editor}
        threads={threads}
        className="w-[350px] block md:hidden"
      />
      <AnchoredThreads
        editor={editor}
        threads={threads}
        className="w-[350px] hidden sm:block"
      />
    </>
  );
}
```

```tsx title="Alternatively use a media query hook" isCollapsed isCollapsable
import { useSyncExternalStore } from "react";
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-blocknote";
import { BlockNoteEditor } from "@blocknote/core";

function ThreadOverlay({ editor }: { editor: BlockNoteEditor | null }) {
  const { threads } = useThreads({ query: { resolved: false } });
  // +++
  const isMobile = useIsMobile();
  // +++

  // +++
  if (isMobile) {
    return (
      <FloatingThreads
        editor={editor}
        threads={threads}
        style={{ width: "350px" }}
      />
    );
  }
  // +++

  // +++
  return (
    <AnchoredThreads
      editor={editor}
      threads={threads}
      style={{ width: "350px" }}
    />
  );
  // +++
}

export function useIsMobile() {
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

function subscribe(callback: () => void) {
  const query = window.matchMedia("(max-width: 1024px)");

  query.addEventListener("change", callback);
  return () => query.removeEventListener("change", callback);
}

function getSnapshot() {
  const query = window.matchMedia("(max-width: 1024px)");
  return query.matches;
}
```

We can place this component inside [`ClientSideSuspense`][] to prevent it
rendering until threads have loaded.

```tsx
<div>
  <BlockNoteView editor={editor} />
  <FloatingComposer editor={editor} style={{ width: "350px" }} />
  // +++
  <ClientSideSuspense fallback={null}>
    <ThreadOverlay editor={editor} />
  </ClientSideSuspense>
  // +++
</div>
```

#### Customization [#FloatingThreads-customization]

The `FloatingThreads` component acts as a wrapper around each individual
[`Thread`][]. You can treat the component like you would a `div`, using classes,
listeners, and more.

```tsx
<FloatingThreads
  editor={editor}
  threads={threads}
  className="my-floating-thread"
/>
```

To apply styling to each [`Thread`][], you can pass a custom `Thread` property
to `components` and modify this in any way. This is the best way to modify a
thread’s width.

```tsx
import { Thread } from "@liveblocks/react-ui";

<FloatingThreads
  editor={editor}
  threads={threads}
  className="my-floating-thread"
  // +++
  components={{
    Thread: (props) => (
      <Thread {...props} className="border shadow" style={{ width: "300px" }} />
    ),
  }}
  // +++
/>;
```

You can return any custom `ReactNode` here, including anything from a simple
wrapper around `Thread`, up to a full custom `Thread` component built using our
[Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment).

```tsx
import { Comment } from "@liveblocks/react-ui/primitives";

<FloatingThreads
  editor={editor}
  threads={threads}
  className="my-floating-thread"
  components={{
    Thread: (props) => (
      // +++
      <div>
        {props.thread.comments.map((comment) => (
          <Comment.Body
            key={comment.id}
            body={comment.body}
            components={/* ... */}
          />
        ))}
      </div>
      // +++
    ),
  }}
/>;
```

#### Props [#FloatingThreads-props]

<PropertiesList>
  <PropertiesListItem name="threads" type="ThreadData[]" required>
    The threads to display.
  </PropertiesListItem>
  <PropertiesListItem
    name="components"
    type="Partial<AnchoredThreadsComponents>"
  >
    Override the component’s components.
  </PropertiesListItem>
  <PropertiesListItem
    name="components.Thread"
    type="(props: ThreadProps) => ReactNode"
  >
    Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread)
    component.
  </PropertiesListItem>
</PropertiesList>

### AnchoredThreads

Displays a list of [`Thread`][] components vertically alongside the editor.

```tsx highlight="3"
<AnchoredThreads editor={editor} threads={threads} />
```

<Figure>
  <Image
    src="/assets/text-editor/anchored-threads.jpg"
    alt="AnchoredThreads"
    width={960}
    height={558}
  />
</Figure>

Takes a list of threads retrieved from [`useThreads`][] and renders them to the
page. Each thread is displayed at the same vertical coordinates as its
corresponding text highlight. If multiple highlights are in the same location,
each thread is placed in order below the previous thread.

```tsx
// +++
import { useThreads } from "@liveblocks/react/suspense";
// +++
import {
  FloatingComposer,
  // +++
  AnchoredThreads,
  // +++
  useCreateBlockNoteWithLiveblocks,
} from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";

function TextEditor() {
  const editor = useCreateBlockNoteWithLiveblocks({});

  // +++
  const { threads } = useThreads();
  // +++

  return (
    <div>
      <BlockNoteView editor={editor} />
      <FloatingComposer editor={editor} style={{ width: "350px" }} />
      // +++
      <AnchoredThreads
        editor={editor}
        threads={threads}
        style={{ width: "350px" }}
      />
      // +++
    </div>
  );
}
```

<Banner title="Resolved threads">

The `AnchoredThreads` component automatically excludes resolved threads from
display. Any resolved threads passed in the threads list will not be shown.

</Banner>

#### Recommended usage [#AnchoredThreads-recommended-usage]

[`FloatingThreads`][] and [`AnchoredThreads`][] have been designed to work
together to provide the optimal experience on mobile and desktop. We generally
recommend using both components, hiding one on smaller screens, as we are below
with Tailwind classes. Most apps also don’t need to display resolved threads, so
we can filter those out with a [`useThreads`][] option.

```tsx
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-blocknote";
import { BlockNoteEditor } from "@blocknote/core";

function ThreadOverlay({ editor }: { editor: BlockNoteEditor | null }) {
  const { threads } = useThreads({ query: { resolved: false } });

  return (
    <>
      <FloatingThreads
        editor={editor}
        threads={threads}
        className="w-[350px] block md:hidden"
      />
      <AnchoredThreads
        editor={editor}
        threads={threads}
        className="w-[350px] hidden sm:block"
      />
    </>
  );
}
```

```tsx title="Alternatively use a media query hook" isCollapsed isCollapsable
import { useSyncExternalStore } from "react";
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-blocknote";
import { BlockNoteEditor } from "@blocknote/core";

function ThreadOverlay({ editor }: { editor: BlockNoteEditor | null }) {
  const { threads } = useThreads({ query: { resolved: false } });
  // +++
  const isMobile = useIsMobile();
  // +++

  // +++
  if (isMobile) {
    return (
      <FloatingThreads
        editor={editor}
        threads={threads}
        style={{ width: "350px" }}
      />
    );
  }
  // +++

  // +++
  return (
    <AnchoredThreads
      editor={editor}
      threads={threads}
      style={{ width: "350px" }}
    />
  );
  // +++
}

export function useIsMobile() {
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

function subscribe(callback: () => void) {
  const query = window.matchMedia("(max-width: 1024px)");

  query.addEventListener("change", callback);
  return () => query.removeEventListener("change", callback);
}

function getSnapshot() {
  const query = window.matchMedia("(max-width: 1024px)");
  return query.matches;
}
```

We can place this component inside [`ClientSideSuspense`][] to prevent it
rendering until threads have loaded.

```tsx
<div>
  <BlockNoteView editor={editor} />
  <FloatingComposer editor={editor} style={{ width: "350px" }} />
  // +++
  <ClientSideSuspense fallback={null}>
    <ThreadOverlay editor={editor} />
  </ClientSideSuspense>
  // +++
</div>
```

#### Customization [#AnchoredThreads-customization]

The `AnchoredThreads` component acts as a wrapper around each [`Thread`][]. It
has no width, so setting this is required, and each thread will take on the
width of the wrapper. You can treat the component like you would a `div`, using
classes, listeners, and more.

```tsx
<AnchoredThreads
  editor={editor}
  threads={threads}
  style={{ width: "350px" }}
  className="my-anchored-thread"
/>
```

To apply styling to each [`Thread`][], you can pass a custom `Thread` property
to `components` and modify this in any way.

```tsx
import { Thread } from "@liveblocks/react-ui";

<AnchoredThreads
  editor={editor}
  threads={threads}
  style={{ width: "350px" }}
  className="my-anchored-thread"
  // +++
  components={{
    Thread: (props) => (
      <Thread
        {...props}
        className="border shadow"
        style={{ background: "white" }}
      />
    ),
  }}
  // +++
/>;
```

You can return any custom `ReactNode` here, including anything from a simple
wrapper around `Thread`, up to a full custom `Thread` component built using our
[Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment).

```tsx
import { Comment } from "@liveblocks/react-ui/primitives";

<AnchoredThreads
  editor={editor}
  threads={threads}
  style={{ width: "350px" }}
  className="my-anchored-thread"
  components={{
    Thread: (props) => (
      // +++
      <div>
        {props.thread.comments.map((comment) => (
          <Comment.Body
            key={comment.id}
            body={comment.body}
            components={/* ... */}
          />
        ))}
      </div>
      // +++
    ),
  }}
/>;
```

##### Modifying thread floating positions

Using CSS variables you can modify the gap between threads, and the horizontal
offset that’s added when a thread is selected.

```css
.lb-tiptap-anchored-threads {
  /* Minimum gap between threads */
  --lb-tiptap-anchored-threads-gap: 8px;

  /* How far the active thread is offset to the left */
  --lb-tiptap-anchored-threads-active-thread-offset: 12px;
}
```

#### Props [#AnchoredThreads-props]

<PropertiesList>
  <PropertiesListItem name="threads" type="ThreadData[]" required>
    The threads to display.
  </PropertiesListItem>
  <PropertiesListItem
    name="components"
    type="Partial<AnchoredThreadsComponents>"
  >
    Override the component’s components.
  </PropertiesListItem>
  <PropertiesListItem
    name="components.Thread"
    type="(props: ThreadProps) => ReactNode"
  >
    Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread)
    component.
  </PropertiesListItem>
</PropertiesList>

### HistoryVersionPreview [@badge=beta]

The `HistoryVersionPreview` component allows you to display a preview of a
specific version of your BlockNote editor’s content. It also contains a button
and logic for restoring. To render a list of versions, see
[`VersionHistory`](/docs/api-reference/liveblocks-react-ui#Version-History).

#### Usage [#HistoryVersionPreview-usage]

```tsx
import { HistoryVersionPreview } from "@liveblocks/react-blocknote";

function VersionPreview({ selectedVersion, onVersionRestore }) {
  return (
    <HistoryVersionPreview
      version={selectedVersion}
      onVersionRestore={onVersionRestore}
    />
  );
}
```

#### Props [#HistoryVersionPreview-props]

<PropertiesList>
  <PropertiesListItem name="version" type="HistoryVersion" required>
    The version of the editor content to preview.
  </PropertiesListItem>
  <PropertiesListItem
    name="onVersionRestore"
    type="(version: HistoryVersion) => void"
  >
    Callback function called when the user chooses to restore this version.
  </PropertiesListItem>
</PropertiesList>

The `HistoryVersionPreview` component renders a read-only view of the specified
version of the editor content. It also provides a button for users to restore
the displayed version.

## Hooks

### useCreateBlockNoteWithLiveblocks

Creates a Liveblocks collaborative BlockNote editor. Use this hook instead of
[`useCreateBlockNote`](https://www.blocknotejs.org/docs/editor-basics/setup#usecreateblocknote-hook).
`editor` should be passed to
[`BlockNoteView`](https://www.blocknotejs.org/docs/editor-basics/setup#rendering-the-editor-with-blocknoteview).

```tsx
import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";

function TextEditor() {
  // +++
  const editor = useCreateBlockNoteWithLiveblocks({});
  // +++

  return (
    <div>
      // +++
      <BlockNoteView editor={editor} />
      // +++
    </div>
  );
}
```

A number of options can be applied to BlockNote and Liveblocks.

```tsx
const editor = useCreateBlockNoteWithLiveblocks(
  {
    // +++
    animations: false,
    trailingBlock: false,
    // +++

    // Other BlockNote options
    // ...
  },
  {
    // +++
    initialContent: "<p>Hello world</p>",
    field: "editor-one",
    // +++

    // Other Liveblocks options
    // ...
  }
);
```

<PropertiesList title="Returns">
  <PropertiesListItem name="liveblocks" type="Extension">
    Returns a [BlockNote
    editor](https://www.blocknotejs.org/docs/editor-basics/setup) with
    collaborative Liveblocks features.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="blockNoteOptions" type="BlockNoteEditorOptions">
    Options to apply to BlockNote. [Learn
    more](https://www.blocknotejs.org/docs/editor-basics/setup#usecreateblocknote-hook).
  </PropertiesListItem>
  <PropertiesListItem name="liveblocksOptions" type="object">
    Options to apply to Liveblocks.
  </PropertiesListItem>
  <PropertiesListItem name="liveblocksOptions.initialContent" type="Content">
    The initial content for the editor, if it’s never been set. [Learn
    more](#Setting-initial-content).
  </PropertiesListItem>
  <PropertiesListItem name="liveblocksOptions.field" type="string">
    The name of this text editor’s field. Allows you to use multiple editors on
    one page, if each has a separate field value. [Learn
    more](#Multiple-editors).
  </PropertiesListItem>
  <PropertiesListItem
    name="liveblocksOptions.offlineSupport_experimental"
    defaultValue="false"
    type="boolean"
  >
    Experimental. Enable offline support using IndexedDB. This means that after
    the first load, documents will be stored locally and load instantly. [Learn
    more](#Offline-support).
  </PropertiesListItem>
  <PropertiesListItem
    name="liveblocksOptions.comments"
    defaultValue="true"
    type="boolean"
  >
    Enable comments in the editor.
  </PropertiesListItem>
  <PropertiesListItem
    name="liveblocksOptions.mentions"
    defaultValue="true"
    type="boolean"
  >
    Enable mentions in the editor.
  </PropertiesListItem>
</PropertiesList>

#### Setting initial content

Initial content for the editor can be set with `initialContent`. This content
will only be used if the current editor has never been edited by any users, and
is ignored otherwise.

```tsx
import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";

function TextEditor() {
  const editor = useCreateBlockNoteWithLiveblocks(
    {},
    {
      // +++
      initialContent: "<p>Hello world</p>",
      // +++
    }
  );

  // ...
}
```

#### Multiple editors

It’s possible to use multiple editors on one page by passing values to the
`field` property. Think of it like an ID for the current editor.

```tsx
import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";

function TextEditor() {
  const editor = useCreateBlockNoteWithLiveblocks(
    {},
    {
      // +++
      field: "editor-one",
      // +++
    }
  );

  // ...
}
```

Here’s an example of how multiple editors may be set up.

```tsx
import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";

function TextEditors() {
  return (
    <div>
      // +++
      <TextEditor field="one" />
      <TextEditor field="two" />
      // +++
    </div>
  );
}

function TextEditor({ field }: { field: string }) {
  const editor = useCreateBlockNoteWithLiveblocks(
    {},
    {
      // +++
      field,
      // +++
    }
  );

  return (
    <div>
      <BlockNoteView editor={editor} />
    </div>
  );
}
```

#### Offline support [@badge=experimental]

It’s possible to enable offline support in your editor with an experimental
option. This means that once a document has been opened, it’s saved locally on
the browser, and can be shown instantly without a loading screen. As soon as
Liveblocks connects, any remote changes will be synchronized, without any load
spinner. Enable this by passing a `offlineSupport_experimental` value.

```tsx
import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";

function TextEditor() {
  const editor = useCreateBlockNoteWithLiveblocks(
    {},
    {
      // +++
      offlineSupport_experimental: true,
      // +++
    }
  );

  // ...
}
```

To make sure that your editor loads instantly, you must structure your app
carefully to avoid any Liveblocks hooks and [`ClientSideSuspense`][] components
from triggering a loading screen. For example, if you’re displaying threads in
your editor with [`useThreads`][], you must place this inside a separate
component and wrap it in [`ClientSideSuspense`][].

```tsx
"use client";

import { ClientSideSuspense, useThreads } from "@liveblocks/react/suspense";
import {
  useCreateBlockNoteWithLiveblocks,
  AnchoredThreads,
  FloatingComposer,
} from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";
import { BlockNoteEditor } from "@blocknote/core";

export function BlockNoteEditor() {
  const editor = useCreateBlockNoteWithLiveblocks(
    {},
    {
      // +++
      offlineSupport_experimental: true,
      // +++
    }
  );

  return (
    <>
      <BlockNoteView editor={editor} />
      <FloatingComposer editor={editor} style={{ width: 350 }} />
      // +++
      <ClientSideSuspense fallback={null}>
        <Threads editor={editor} />
      </ClientSideSuspense>
      // +++
    </>
  );
}

// +++
function Threads({ editor }: { editor: BlockNoteEditor }) {
  const { threads } = useThreads();

  return <AnchoredThreads editor={editor} threads={threads} />;
}
// +++
```

### useIsEditorReady

Used to check if the editor content has been loaded or not, helpful for
displaying a loading skeleton.

```ts
import { useIsEditorReady } from "@liveblocks/react-blocknote";

const status = useIsEditorReady();
```

Here's how it can be used in the context of your editor.

```tsx
import {
  useCreateBlockNoteWithLiveblocks,
  useIsEditorReady,
} from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";

function TextEditor() {
  const editor = useCreateBlockNoteWithLiveblocks({});
  // +++
  const ready = useIsEditorReady();
  // +++

  return (
    <div>
      // +++
      {!ready ? <div>Loading...</div> : <BlockNoteView editor={editor} />}
      // +++
    </div>
  );
}
```

## Stylesheets

React BlockNote comes with default styles, and these can be imported into the
root of your app or directly into a CSS file with `@import`. Note that you must
also install and import a stylesheet from
[`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui) to use these
styles.

```tsx
import "@liveblocks/react-ui/styles.css";
import "@liveblocks/react-blocknote/styles.css";
```

### Customizing your styles

Adding dark mode and customizing your styles is part of `@liveblocks/react-ui`,
learn how to do this under
[styling and customization](/docs/api-reference/liveblocks-react-ui#Styling-and-customization).

[`Thread`]: /docs/api-reference/liveblocks-react-ui#Thread
[`Composer`]: /docs/api-reference/liveblocks-react-ui#Composer
[`useThreads`]: /docs/api-reference/liveblocks-react#useThreads
[`Icon`]: /docs/api-reference/liveblocks-react-ui#Icon
[`FloatingToolbar`]: #FloatingToolbar
[`FloatingComposer`]: #FloatingComposer
[`FloatingThreads`]: #FloatingThreads
[`AnchoredThreads`]: #AnchoredThreads
[`ClientSideSuspense`]: /docs/api-reference/liveblocks-react#ClientSideSuspense

---
meta:
  title: "@liveblocks/react-lexical"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/react-lexical package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/react-lexical` provides you with a [React](https://react.dev/)
plugin that adds collaboration to any [Lexical](https://lexical.dev/) text
editor. It also adds realtime cursors, document persistence on the cloud,
comments, and mentions. Read our
[get started guides](/docs/get-started/text-editor/lexical) to learn more.

## Setup

To set up your collaborative Lexical editor, you must use
[`LiveblocksPlugin`](#LiveblocksPlugin) and
[`liveblocksConfig`](#liveblocksConfig).

### LiveblocksPlugin

Liveblocks plugin for Lexical that adds collaboration to your editor.

```tsx highlight="2"
<LexicalComposer initialConfig={initialConfig}>
  <LiveblocksPlugin />
</LexicalComposer>
```

{/* TODO: Image */}

`LiveblocksPlugin` should always be nested inside [`LexicalComposer`][], and
each [Lexical default component](#Default-components) you’re using should be
placed inside the plugin.

```tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
// +++
import { liveblocksConfig, LiveblocksPlugin } from "@liveblocks/react-lexical";
// +++

const initialConfig = liveblocksConfig({
  namespace: "MyEditor",
  theme: {},
  nodes: [],
  onError: (err) => console.error(err),
});

function Editor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      // +++
      <LiveblocksPlugin>
        <FloatingThreads />
        <FloatingComposer />
        <AnchoredThreads />
      </LiveblocksPlugin>
      // +++
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Enter some text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
    </LexicalComposer>
  );
}
```

<Banner title="Resolved threads">

Annotations associated with resolved threads are hidden by default on the
editor.

</Banner>

Learn more in our [get started guides](/docs/get-started/text-editor/lexical).

### liveblocksConfig

Function that takes a Lexical editor config and modifies it to add the necessary
`nodes` and `theme` to make [`LiveblocksPlugin`][] works correctly.

```tsx
import { liveblocksConfig } from "@liveblocks/react-lexical";

const initialConfig = liveblocksConfig({
  namespace: "MyEditor",
  theme: {},
  nodes: [],
  onError: (err) => console.error(err),
});
```

The config created by `liveblocksConfig` should be passed to `initialConfig` in
[`LexicalComposer`][].

```tsx highlight="7-12,16"
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { liveblocksConfig, LiveblocksPlugin } from "@liveblocks/react-lexical";

const initialConfig = liveblocksConfig({
  namespace: "MyEditor",
  theme: {},
  nodes: [],
  onError: (err) => console.error(err),
});

function Editor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <LiveblocksPlugin>
        <FloatingThreads />
        <FloatingComposer />
        <AnchoredThreads />
      </LiveblocksPlugin>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Enter some text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
    </LexicalComposer>
  );
}
```

Note that `liveblocksConfig` sets `editorState` to `null` because
`LiveblocksPlugin` is responsible for initializing it on the server.

## Default components

### Toolbar

Displays a toolbar, allowing you to change the styles of selected text. You can
add content [before or after](#toolbar-extending-the-defaults), or the toolbar’s
options can be [customized](#creating-a-custom-toolbar). A
[floating toolbar](#FloatingToolbar) also exists.

```tsx
<LexicalComposer initialConfig={initialConfig}>
  <LiveblocksPlugin>
    // +++
    <Toolbar />
    // +++
  </LiveblocksPlugin>
</LexicalComposer>
```

<Figure>
  <Image
    src="/assets/text-editor/toolbar.png"
    alt="Toolbar"
    width={960}
    height={558}
  />
</Figure>

By default, one of the toolbar buttons can create comment threads—to enable this
add [`FloatingComposer`][] and display threads with [`AnchoredThreads`][] or
[`FloatingThreads`][]. Should be nested inside [`LiveblocksPlugin`][].

```tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { useThreads } from "@liveblocks/react/suspense";
import {
  liveblocksConfig,
  LiveblocksPlugin,
  FloatingComposer,
  // +++
  Toolbar,
  // +++
} from "@liveblocks/react-lexical";

const initialConfig = liveblocksConfig({
  namespace: "MyEditor",
  theme: {},
  nodes: [],
  onError: (err) => console.error(err),
});

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

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <LiveblocksPlugin>
        // +++
        <Toolbar />
        // +++
        <FloatingComposer style={{ width: "350px" }} />
        <FloatingThreads threads={threads} style={{ width: "350px" }} />
      </LiveblocksPlugin>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Enter some text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
    </LexicalComposer>
  );
}
```

#### Extending the defaults [#toolbar-extending-the-defaults]

You can insert content `before` the first button and `after` the last button
using `before` and `after`. Components such as [`Toolbar.Button`][] and
[`Toolbar.Toggle`][] can be used to create new buttons.

```tsx
import { Toolbar } from "@liveblocks/react-lexical";
import { Icon } from "@liveblocks/react-ui";

<Toolbar
  // +++
  before={<>I'm at the start</>}
  after={
    <Toolbar.Button
      name="Help"
      icon={<Icon.QuestionMark />}
      shortcut="CMD-H"
      onClick={() => console.log("help")}
    />
  }
  // +++
/>;
```

For more complex customization, instead read
[creating a custom floating toolbar](#creating-a-custom-toolbar).

#### Creating a custom toolbar [#creating-a-custom-toolbar]

By passing elements as children, it’s possible to create a fully custom toolbar.

```tsx highlight="6"
import { Toolbar } from "@liveblocks/react-lexical";

function CustomToolbar() {
  return (
    <Toolbar>
      Hello <strong>world</strong>
    </Toolbar>
  );
}
```

Each part of our default toolbar is available as blocks which can be slotted
together. This is how the default toolbar is constructed:

```tsx
import { Toolbar } from "@liveblocks/react-lexical";

function CustomToolbar() {
  return (
    <Toolbar>
      // +++
      <Toolbar.BlockSelector />
      <Toolbar.SectionInline />
      <Toolbar.Separator />
      <Toolbar.SectionCollaboration />
      // +++
    </Toolbar>
  );
}
```

You can mix these default components with any custom ones of your own. Below the
[`Toolbar.SectionHistory`][] component is added alongside some custom buttons
created with [`Toolbar.Button`][], [`Toolbar.Toggle`][], and [`Icon`][].

```tsx
import { FORMAT_TEXT_COMMAND } from "lexical";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { isTextFormatActive, Toolbar } from "@liveblocks/react-lexical";
import { Icon } from "@liveblocks/react-ui";

function CustomToolbar() {
  const [editor] = useLexicalComposerContext();

  return (
    <Toolbar>
      // +++
      <Toolbar.SectionHistory />
      <Toolbar.Separator />
      <Toolbar.Button
        name="Help"
        icon={<Icon.QuestionMark />}
        shortcut="CMD-H"
        onClick={() => console.log("help")}
      />
      <Toolbar.Toggle
        name="Bold"
        icon={<strong>B️</strong>}
        active={isTextFormatActive(editor, "bold")}
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")}
      />
      // +++
    </Toolbar>
  );
}
```

To learn more about the different components, read more below.

#### Props [#Toolbar-props]

<PropertiesList>
  <PropertiesListItem name="editor" type="LexicalEditor" required>
    The Lexical editor.
  </PropertiesListItem>
  <PropertiesListItem name="children" type="ReactNode">
    The content of the toolbar, overriding the default content. Use the `before`
    and `after` props if you want to keep and extend the default content. Any
    `ReactNode` or `Toolbar.*` components work inside.
  </PropertiesListItem>
  <PropertiesListItem name="before" type="ReactNode">
    The content to display at the start of the toolbar. Any `ReactNode` or
    `Toolbar.*` components work inside.
  </PropertiesListItem>
  <PropertiesListItem name="after" type="ReactNode">
    The content to display at the end of the toolbar. Any `ReactNode` or
    `Toolbar.*` components work inside.
  </PropertiesListItem>
</PropertiesList>

#### Toolbar.Button

A button for triggering actions. The `name` is displayed in a tooltip. Props
such as `onClick` will be passed to the underlying `button` element.

```tsx
import { Toolbar } from "@liveblocks/react-lexical";

<Toolbar>
  <Toolbar.Button name="Question" onClick={(e) => console.log("Clicked")} />
</Toolbar>;
```

Optionally takes an icon which will visually replace the `name`. Also optionally
accepts a shortcut, which is displayed in the tooltip. Comment key names are
converted to symbols. Here are various examples.

```tsx
import { Toolbar } from "@liveblocks/react-lexical";
import { Icon } from "@liveblocks/react-ui";

// Button says "Question"
<Toolbar.Toggle name="Question" onClick={/* ... */} />

// Tooltip says "Question [⌘+Q]"
<Toolbar.Button name="Question" shortcut="CMD+Q" onClick={/* ... */} />

// Custom icon, replaces the name in the button
<Toolbar.Button name="Question" icon={<div>?</div>} onClick={/* ... */} />

// Using a Liveblocks icon, replaces the name in the button
<Toolbar.Button name="Question" icon={<Icon.QuestionMark />} onClick={/* ... */} />

// Passing children visually replaces the `name` and `icon`
<Toolbar.Button name="Question" onClick={/* ... */}>
  ? Ask a question
</Toolbar.Button>

// Props are passed to the inner `button`
<Toolbar.Button
  name="Question"
  style={{ marginLeft: 10 }}
  className="custom-button"
  onMouseOver={() => console.log("Hovered")}
/>
```

##### Props [#ToolbarButton-props]

<PropertiesList>
  <PropertiesListItem name="name" type="string" required>
    The name of this button displayed in its tooltip. Will also be displayed in
    the button if no `icon` or `children` are passed.
  </PropertiesListItem>
  <PropertiesListItem name="icon" type="ReactNode">
    An optional icon displayed in this button.
  </PropertiesListItem>
  <PropertiesListItem name="shortcut" type="string">
    An optional keyboard shortcut displayed in this button’s tooltip. Common
    shortcuts such will be replaced by their symbols, for example `CMD` → `⌘`.
  </PropertiesListItem>
</PropertiesList>

#### Toolbar.Toggle

A toggle button for values that can be active or inactive. Best used with text
editor commands. The `name` is displayed in a tooltip. Props will be passed to
the underlying `button` element.

```tsx
import { FORMAT_TEXT_COMMAND } from "lexical";
import { isTextFormatActive, Toolbar } from "@liveblocks/react-lexical";

<Toolbar>
  <Toolbar.Toggle
    name="Highlight"
    active={isTextFormatActive(editor, "bold")}
    onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")}
  />
</Toolbar>;
```

The snippet above shows how to toggle bold styling. The toggle button can also
be toggled with `useState`.

```tsx
import { Toolbar } from "@liveblocks/react-lexical";
import { useState } from "react";

function CustomToggle() {
  const [active, setActive] = useState(false);

  return (
    <Toolbar.Toggle
      name="Toggle options"
      active={active}
      onClick={() => setActive(!active)}
    />
  );
}
```

`Toolbar.Toggle` optionally takes an icon which will visually replace the
`name`. Also optionally accepts a shortcut, which is displayed in the tooltip.
Comment key names are converted to symbols. Here are various examples.

```tsx
import { Toolbar } from "@liveblocks/react-lexical";
import { Icon } from "@liveblocks/react-ui";

// Button says "Highlight"
<Toolbar.Toggle
  name="Highlight"
  active={/* ... */}
  onClick={/* ... */}
/>

// Tooltip says "Highlight [⌘+H]"
<Toolbar.Toggle
  name="Highlight"
  shortcut="CMD+H"
  active={/* ... */}
  onClick={/* ... */}
/>

// Custom icon, replaces the name in the button
<Toolbar.Toggle
  name="Highlight"
  icon={<div>🖊</div>}
  active={/* ... */}
  onClick={/* ... */}
/>

// Using a Liveblocks icon, replaces the name in the button
<Toolbar.Toggle
  name="Highlight"
  icon={<Icon.QuestionMark />}
  active={/* ... */}
  onClick={/* ... */}
/>

// Passing children visually replaces the `name` and `icon`
<Toolbar.Toggle
  name="Highlight"
  active={/* ... */}
  onClick={/* ... */}
>
  🖊️Highlight
</Toolbar.Toggle>

// Props are passed to the inner `button`
<Toolbar.Toggle
  name="Highlight"
  active={/* ... */}
  onClick={/* ... */}
  style={{ marginLeft: 10 }}
  className="custom-toggle"
  onMouseOver={() => console.log("Hovered")}
/>
```

##### Props [#ToolbarToggle-props]

<PropertiesList>
  <PropertiesListItem name="name" type="string" required>
    The name of this button displayed in its tooltip. Will also be displayed in
    the button if no `icon` or `children` are passed.
  </PropertiesListItem>
  <PropertiesListItem name="active" type="boolean" required>
    Whether the button is toggled.
  </PropertiesListItem>
  <PropertiesListItem name="icon" type="ReactNode">
    An optional icon displayed in this button.
  </PropertiesListItem>
  <PropertiesListItem name="shortcut" type="string">
    An optional keyboard shortcut displayed in this button’s tooltip. Common
    shortcuts such will be replaced by their symbols, for example `CMD` → `⌘`.
  </PropertiesListItem>
</PropertiesList>

#### Toolbar.BlockSelector

Adds a dropdown selector for switching between different block types, such as
_text_, _heading 1_, _blockquote_. Props will be passed to the inner `button`
element. Can also be placed inside [`FloatingToolbar`][].

```tsx
import { Toolbar } from "@liveblocks/react-lexical";

<Toolbar>
  <Toolbar.BlockSelector />
</Toolbar>;
```

##### Use custom item options

If you’d like to change the items shown in the dropdown menu, you can pass a
custom `items` array. Below a code block item
([Lexical extension](https://lexical.dev/docs/api/modules/lexical_code)) is
added after the default options.

```tsx
import { isBlockNodeActive, Toolbar } from "@liveblocks/react-lexical";
import { $setBlocksType } from "@lexical/selection";
import { $isCodeNode } from "@lexical/code";
import { $getSelection } from "lexical";

<Toolbar>
  <Toolbar.BlockSelector
    items={(defaultItems) => [
      ...defaultItems,
      {
        name: "Code block",
        icon: <div>❮ ❯</div>, // Optional
        isActive: (editor) => isBlockNodeActive(editor, $isCodeNode),
        setActive: (editor) =>
          $setBlocksType($getSelection(), () => $createCodeNode()),
      },
    ]}
  />
</Toolbar>;
```

##### Customize item styles

By passing a `label` property, you can overwrite the styles of the dropdown
items. The toolbar button will still display the `name`, but in the dropdown,
the `label` will be used instead of the `name` and `icon`. Below, a new item is
added and its `label` is customized.

```tsx
import { isBlockNodeActive, Toolbar } from "@liveblocks/react-lexical";
import { $setBlocksType } from "@lexical/selection";
import { $isCodeNode } from "@lexical/code";
import { $getSelection } from "lexical";

<Toolbar>
  <Toolbar.BlockSelector
    items={(defaultItems) => [
      ...defaultItems,
      {
        name: "Code block",
        // +++
        label: <div className="font-mono">Code</div>, // Optional, overwrites `icon` + `name`
        // +++
        isActive: (editor) => isBlockNodeActive(editor, $isCodeNode),
        setActive: (editor) =>
          $setBlocksType($getSelection(), () => $createCodeNode()),
      },
    ]}
  />
</Toolbar>;
```

You can also customize the default items. Below each item is styled to represent
the effect each block applies to the document.

```tsx
import { Toolbar } from "@liveblocks/react-lexical";

<Toolbar.BlockSelector
  items={(defaultItems) =>
    defaultItems.map((item) => {
      let label;

      if (item.name === "Text") {
        label = <span>Regular text</span>;
      }

      if (item.name === "Heading 1") {
        label = (
          <span style={{ fontSize: 18, fontWeight: "bold" }}>Heading 1</span>
        );
      }

      if (item.name === "Heading 2") {
        label = (
          <span style={{ fontSize: 16, fontWeight: "bold" }}>Heading 2</span>
        );
      }

      if (item.name === "Heading 3") {
        label = (
          <span style={{ fontSize: 15, fontWeight: "bold" }}>Heading 3</span>
        );
      }

      if (item.name === "Blockquote") {
        label = (
          <span style={{ borderLeft: "3px solid gray", paddingLeft: 8 }}>
            Blockquote
          </span>
        );
      }

      return {
        ...item,
        label,
        icon: null, // Hide all icons
      };
    })
  }
/>;
```

##### Props [#ToolbarBlockSelector-props]

<PropertiesList>
  <PropertiesListItem
    name="items"
    type="array | function"
    detailedType="ToolbarBlockSelectorItem[] | (defaultItems: ToolbarBlockSelectorItem[]) => ToolbarBlockSelectorItem[]"
  >
    The items displayed in this block selector. When provided as an array, the
    default items are overridden. To avoid this, a function can be provided
    instead and it will receive the default items.
  </PropertiesListItem>
</PropertiesList>

#### Toolbar.Separator

Adds a visual, and accessible, separator used to separate sections in the
toolbar. Props will be passed to the inner `div` element. Can also be placed
inside [`FloatingToolbar`][].

```tsx
import { Toolbar } from "@liveblocks/react-lexical";

<Toolbar>
  // +++
  <Toolbar.SectionHistory />
  // +++
</Toolbar>;
```

#### Toolbar.SectionHistory

Adds a section containing _undo_ and _redo_ buttons. Can also be placed inside
[`FloatingToolbar`][].

```tsx
import { Toolbar } from "@liveblocks/react-lexical";

<Toolbar>
  // +++
  <Toolbar.SectionHistory />
  // +++
</Toolbar>;
```

#### Toolbar.SectionInline

Adds a section containing inline formatting actions such as _bold_, _italic_,
_underline_. Can also be placed inside [`FloatingToolbar`][].

```tsx
import { Toolbar } from "@liveblocks/react-lexical";

<Toolbar>
  // +++
  <Toolbar.SectionInline />
  // +++
</Toolbar>;
```

#### Toolbar.SectionCollaboration

Adds a section containing an _add comment_ button. Can also be placed inside
[`FloatingToolbar`][].

```tsx
import { Toolbar } from "@liveblocks/react-lexical";

<Toolbar>
  // +++
  <Toolbar.SectionCollaboration />
  // +++
</Toolbar>;
```

### FloatingToolbar

Displays a floating toolbar near the current Lexical selection, allowing you to
change styles. You can add content
[before or after](#floating-toolbar-extending-the-defaults), or the toolbar’s
options can be [customized](#creating-a-custom-floating-toolbar). A
[static toolbar](#Toolbar) also exists.

```tsx
<LexicalComposer initialConfig={initialConfig}>
  <LiveblocksPlugin>
    // +++
    <FloatingToolbar />
    // +++
  </LiveblocksPlugin>
</LexicalComposer>
```

<Figure>
  <Image
    src="/assets/text-editor/floating-toolbar.png"
    alt="FloatingToolbar"
    width={960}
    height={558}
  />
</Figure>

By default, one of the toolbar buttons can create comment threads—to enable this
add [`FloatingComposer`][] and display threads with [`AnchoredThreads`][] or
[`FloatingThreads`][]. Should be nested inside [`LiveblocksPlugin`][].

```tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { useThreads } from "@liveblocks/react/suspense";
import {
  liveblocksConfig,
  LiveblocksPlugin,
  FloatingComposer,
  // +++
  FloatingToolbar,
  // +++
} from "@liveblocks/react-lexical";

const initialConfig = liveblocksConfig({
  namespace: "MyEditor",
  theme: {},
  nodes: [],
  onError: (err) => console.error(err),
});

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

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <LiveblocksPlugin>
        // +++
        <FloatingToolbar />
        // +++
        <FloatingComposer style={{ width: "350px" }} />
        <FloatingThreads threads={threads} style={{ width: "350px" }} />
      </LiveblocksPlugin>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Enter some text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
    </LexicalComposer>
  );
}
```

#### Changing float position

Using `position` and `offset` you can reposition the toolbar relative to the
current selection. `position` can be set to `"top"` or `"bottom"`, and `offset`
defines the vertical distance in pixels from the selection.

```tsx
<FloatingToolbar
  // +++
  position="bottom" // Position can be `top` or `bottom`
  offset={12} // Distance in px from selection
  // +++
/>
```

#### Extending the defaults [#floating-toolbar-extending-the-default]

You can insert custom content `before` the first button and `after` the last
button using `before` and `after`. Components such as [`Toolbar.Button`][] and
[`Toolbar.Toggle`][] can be used to create new buttons.

```tsx
import { FloatingToolbar } from "@liveblocks/react-lexical";
import { Icon } from "@liveblocks/react-ui";

<FloatingToolbar
  // +++
  before={<>I'm at the start</>}
  after={
    <Toolbar.Button
      name="Help"
      icon={<Icon.QuestionMark />}
      shortcut="CMD-H"
      onClick={() => console.log("help")}
    />
  }
  // +++
/>;
```

For more complex customization, instead read
[creating a custom floating toolbar](#creating-a-custom-floating-toolbar).

#### Creating a custom floating toolbar [#creating-a-custom-floating-toolbar]

By passing elements as children, it’s possible to create a fully custom floating
toolbar.

```tsx highlight="7"
import { FloatingToolbar } from "@liveblocks/react-lexical";

function CustomToolbar() {
  return (
    <FloatingToolbar>
      Hello <strong>world</strong>
    </FloatingToolbar>
  );
}
```

Each part of our default toolbar is available as blocks which can be slotted
together. This is how the default floating toolbar is constructed:

```tsx
import { FloatingToolbar } from "@liveblocks/react-lexical";

function CustomToolbar() {
  return (
    <FloatingToolbar>
      // +++
      <Toolbar.BlockSelector />
      <Toolbar.SectionInline />
      <Toolbar.Separator />
      <Toolbar.SectionCollaboration />
      // +++
    </FloatingToolbar>
  );
}
```

You can mix these default components with any custom ones of your own. Below the
[`Toolbar.SectionHistory`][] component is added alongside some custom buttons
created with [`Toolbar.Button`][], [`Toolbar.Toggle`][], and [`Icon`][].

```tsx
import { FORMAT_TEXT_COMMAND } from "lexical";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { isTextFormatActive, FloatingToolbar } from "@liveblocks/react-lexical";
import { Icon } from "@liveblocks/react-ui";

function CustomToolbar() {
  const [editor] = useLexicalComposerContext();

  return (
    <FloatingToolbar>
      // +++
      <Toolbar.SectionHistory />
      <Toolbar.Separator />
      <Toolbar.Button
        name="Help"
        icon={<Icon.QuestionMark />}
        shortcut="CMD-H"
        onClick={() => console.log("help")}
      />
      <Toolbar.Toggle
        name="Bold"
        icon={<strong>B️</strong>}
        active={isTextFormatActive(editor, "bold")}
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")}
      />
      // +++
    </FloatingToolbar>
  );
}
```

To learn more about the different components, read more under [`Toolbar`][].

#### Props [#FloatingToolbar-props]

<PropertiesList>
  <PropertiesListItem name="editor" type="LexicalEditor" required>
    The Lexical editor.
  </PropertiesListItem>
  <PropertiesListItem name="position" type={`"top" | "bottom"`}>
    The vertical position of the floating toolbar.
  </PropertiesListItem>
  <PropertiesListItem name="offset" type="number">
    The vertical offset of the floating toolbar from the selection.
  </PropertiesListItem>
  <PropertiesListItem name="children" type="ReactNode">
    The content of the toolbar, overriding the default content. Use the `before`
    and `after` props if you want to keep and extend the default content. Any
    `ReactNode` or `Toolbar.*` components work inside.
  </PropertiesListItem>
  <PropertiesListItem name="before" type="ReactNode">
    The content to display at the start of the toolbar. Any `ReactNode` or
    `Toolbar.*` components work inside.
  </PropertiesListItem>
  <PropertiesListItem name="after" type="ReactNode">
    The content to display at the end of the toolbar. Any `ReactNode` or
    `Toolbar.*` components work inside.
  </PropertiesListItem>
</PropertiesList>

### FloatingComposer

Displays a [`Composer`][] near the current Lexical selection, allowing you to
create threads.

```tsx highlight="3"
<LexicalComposer initialConfig={initialConfig}>
  <LiveblocksPlugin>
    <FloatingComposer />
  </LiveblocksPlugin>
</LexicalComposer>
```

<Figure>
  <Image
    src="/assets/text-editor/floating-composer.jpg"
    alt="FloatingComposer"
    width={960}
    height={558}
  />
</Figure>

Submitting a comment will attach an annotation thread at the current selection.
Should be nested inside [`LiveblocksPlugin`][].

```tsx highlight="8,22"
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import {
  liveblocksConfig,
  LiveblocksPlugin,
  FloatingComposer,
} from "@liveblocks/react-lexical";

const initialConfig = liveblocksConfig({
  namespace: "MyEditor",
  theme: {},
  nodes: [],
  onError: (err) => console.error(err),
});

function Editor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <LiveblocksPlugin>
        <FloatingComposer style={{ width: "350px" }} />
      </LiveblocksPlugin>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Enter some text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
    </LexicalComposer>
  );
}
```

Display created threads with [`AnchoredThreads`][] or [`FloatingThreads`][].

#### Opening the composer

To open the `FloatingComposer`, you need to dispatch the
`OPEN_FLOATING_COMPOSER_COMMAND`
[Lexical command](https://lexical.dev/docs/concepts/commands).

```tsx highlight="10"
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { OPEN_FLOATING_COMPOSER_COMMAND } from "@liveblocks/react-lexical";

function Toolbar() {
  const [editor] = useLexicalComposerContext();

  return (
    <button
      onClick={() => {
        editor.dispatchCommand(OPEN_FLOATING_COMPOSER_COMMAND, undefined);
      }}
    >
      💬 New comment
    </button>
  );
}
```

#### Customization [#FloatingComposer-customization]

The `FloatingComposer` component acts as a wrapper around a [`Composer`][], near
the current selection. You can treat the component like you would a `form`,
using classes, listeners, and more.

```tsx
<FloatingComposer style={{ width: "350px" }} className="my-floating-composer" />
```

To apply styling to the composer, you can pass a custom `Composer` property to
`components` and modify this in any way.

```tsx
import { Composer } from "@liveblocks/react-ui";

<FloatingComposer
  style={{ width: "350px" }}
  // +++
  components={{
    Composer: (props) => (
      <Composer
        {...props}
        className="border shadow"
        style={{ width: "300px" }}
      />
    ),
  }}
  // +++
/>;
```

You can return any custom `ReactNode` here, including anything from a simple
wrapper around `Composer`, up to a full custom `Composer` component built using
our
[Composer primitives](/docs/api-reference/liveblocks-react-ui#primitives-Composer).

```tsx
import { Composer } from "@liveblocks/react-ui/primitives";

<FloatingComposer
  style={{ width: "350px" }}
  components={{
    // +++
    Composer: ({ onComposerSubmit }) => (
      <Composer.Form onComposerSubmit={onComposerSubmit}>
        <Composer.Editor />
        <Composer.Submit>Send</Composer.Submit>
      </Composer.Form>
    ),
    // +++
  }}
/>;
```

You can also customize submission behavior by passing a custom
`onComposerSubmit` function to the `Composer.Form` component.

```tsx
import { Composer } from "@liveblocks/react-ui/primitives";

<FloatingComposer
  editor={editor}
  style={{ width: "350px" }}
  components={{
    Composer: () => (
      <Composer.Form
        // +++
          onComposerSubmit={(comment, event) => {
            event.preventDefault();

            const thread = createThread({
              body: comment.body,
              attachments: comment.attachments,
              metadata: ...,
            });

            editor.dispatchCommand(ATTACH_THREAD_COMMAND, thread.id);
          }}
          // +++
        >
          <Composer.Editor />
          <Composer.Submit>Send</Composer.Submit>
        </Composer.Form>
      )
  }}
/>;
```

#### Props [#FloatingComposer-props]

<PropertiesList>
  <PropertiesListItem name="metadata" type="ThreadMetadata">
    The metadata of the thread to create.
  </PropertiesListItem>
  <PropertiesListItem name="commentMetadata" type="CommentMetadata">
    The metadata of the comment to create.
  </PropertiesListItem>
  <PropertiesListItem
    name="onComposerSubmit"
    type="function"
    detailedType="(comment: ComposerSubmitComment, event: FormEvent<HTMLFormElement>) => Promise<void> | void"
  >
    The event handler called when the composer is submitted.
  </PropertiesListItem>
  <PropertiesListItem name="defaultValue" type="CommentBody">
    The composer’s initial value.
  </PropertiesListItem>
  <PropertiesListItem name="collapsed" type="boolean">
    Whether the composer is collapsed. Setting a value will make the composer
    controlled.
  </PropertiesListItem>
  <PropertiesListItem
    name="onCollapsedChange"
    type="function"
    detailedType="(collapsed: boolean) => void"
  >
    The event handler called when the collapsed state of the composer changes.
  </PropertiesListItem>
  <PropertiesListItem name="defaultCollapsed" type="boolean">
    Whether the composer is initially collapsed. Setting a value will make the
    composer uncontrolled.
  </PropertiesListItem>
  <PropertiesListItem name="disabled" type="boolean">
    Whether the composer is disabled.
  </PropertiesListItem>
  <PropertiesListItem name="autoFocus" type="boolean">
    Whether to focus the composer on mount.
  </PropertiesListItem>
  <PropertiesListItem
    name="overrides"
    type="Partial<GlobalOverrides & ComposerOverrides>"
  >
    Override the component’s strings.
  </PropertiesListItem>
  <PropertiesListItem
    name="components"
    type="Partial<FloatingComposerComponents>"
  >
    Override the component’s components.
  </PropertiesListItem>
  <PropertiesListItem
    name="components.Composer"
    type="(props: ComposerProps) => ReactNode"
  >
    Override the [`Composer`](/docs/api-reference/liveblocks-react-ui#Composer)
    component.
  </PropertiesListItem>
</PropertiesList>

### FloatingThreads

Displays floating [`Thread`][] components below text highlights in the editor.

```tsx highlight="3"
<LexicalComposer initialConfig={initialConfig}>
  <LiveblocksPlugin>
    <FloatingThreads threads={threads} />
  </LiveblocksPlugin>
</LexicalComposer>
```

<Figure>
  <Image
    src="/assets/text-editor/floating-threads.jpg"
    alt="FloatingThreads"
    width={960}
    height={558}
  />
</Figure>

Takes a list of threads retrieved from [`useThreads`][] and renders them to the
page. Each thread is opened by clicking on its corresponding text highlight.

```tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { useThreads } from "@liveblocks/react/suspense";
import {
  liveblocksConfig,
  LiveblocksPlugin,
  FloatingComposer,
  // +++
  FloatingThreads,
  // +++
} from "@liveblocks/react-lexical";

const initialConfig = liveblocksConfig({
  namespace: "MyEditor",
  theme: {},
  nodes: [],
  onError: (err) => console.error(err),
});

function Editor() {
  // +++
  const { threads } = useThreads();
  // +++

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <LiveblocksPlugin>
        <FloatingComposer />
        // +++
        <FloatingThreads threads={threads} style={{ width: "350px" }} />
        // +++
      </LiveblocksPlugin>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Enter some text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
    </LexicalComposer>
  );
}
```

Should be nested inside [`LiveblocksPlugin`][].

<Banner title="Resolved threads">

The `FloatingThreads` component automatically excludes resolved threads from
display. Any resolved threads passed in the threads list will not be shown.

</Banner>

#### Recommended usage [#FloatingThreads-recommended-usage]

[`FloatingThreads`][] and [`AnchoredThreads`][] have been designed to work
together to provide the optimal experience on mobile and desktop. We generally
recommend using both components, hiding one on smaller screens, as we are below
with Tailwind classes. Most apps also don’t need to display resolved threads, so
we can filter those out with a [`useThreads`][] option.

```tsx
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-lexical";

function ThreadOverlay() {
  const { threads } = useThreads({ query: { resolved: false } });

  return (
    <>
      <FloatingThreads
        threads={threads}
        className="w-[350px] block md:hidden"
      />
      <AnchoredThreads
        threads={threads}
        className="w-[350px] hidden sm:block"
      />
    </>
  );
}
```

```tsx title="Alternatively use a media query hook" isCollapsed isCollapsable
import { useSyncExternalStore } from "react";
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-lexical";

function ThreadOverlay() {
  const { threads } = useThreads({ query: { resolved: false } });
  // +++
  const isMobile = useIsMobile();
  // +++

  // +++
  if (isMobile) {
    return <FloatingThreads threads={threads} style={{ width: "350px" }} />;
  }
  // +++

  // +++
  return <AnchoredThreads threads={threads} style={{ width: "350px" }} />;
  // +++
}

export function useIsMobile() {
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

function subscribe(callback: () => void) {
  const query = window.matchMedia("(max-width: 1024px)");

  query.addEventListener("change", callback);
  return () => query.removeEventListener("change", callback);
}

function getSnapshot() {
  const query = window.matchMedia("(max-width: 1024px)");
  return query.matches;
}
```

We can place this component inside [`ClientSideSuspense`][] to prevent it
rendering until threads have loaded.

```tsx
<LexicalComposer initialConfig={initialConfig}>
  <LiveblocksPlugin>
    <FloatingComposer />
    // +++
    <ClientSideSuspense fallback={null}>
      <ThreadOverlay />
    </ClientSideSuspense>
    // +++
  </LiveblocksPlugin>
</LexicalComposer>
```

#### Customization [#FloatingThreads-customization]

The `FloatingThreads` component acts as a wrapper around each individual
[`Thread`][]. You can treat the component like you would a `div`, using classes,
listeners, and more.

```tsx
<FloatingThreads threads={threads} className="my-floating-thread" />
```

To apply styling to each [`Thread`][], you can pass a custom `Thread` property
to `components` and modify this in any way. This is the best way to modify a
thread’s width.

```tsx
import { Thread } from "@liveblocks/react-ui";

<FloatingThreads
  threads={threads}
  className="my-floating-thread"
  // +++
  components={{
    Thread: (props) => (
      <Thread {...props} className="border shadow" style={{ width: "300px" }} />
    ),
  }}
  // +++
/>;
```

You can return any custom `ReactNode` here, including anything from a simple
wrapper around `Thread`, up to a full custom `Thread` component built using our
[Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment).

```tsx
import { Comment } from "@liveblocks/react-ui/primitives";

<FloatingThreads
  threads={threads}
  className="my-floating-thread"
  components={{
    Thread: (props) => (
      // +++
      <div>
        {props.thread.comments.map((comment) => (
          <Comment.Body
            key={comment.id}
            body={comment.body}
            components={/* ... */}
          />
        ))}
      </div>
      // +++
    ),
  }}
/>;
```

#### Props [#FloatingThreads-props]

<PropertiesList>
  <PropertiesListItem name="threads" type="ThreadData[]" required>
    The threads to display.
  </PropertiesListItem>
  <PropertiesListItem
    name="components"
    type="Partial<AnchoredThreadsComponents>"
  >
    Override the component’s components.
  </PropertiesListItem>
  <PropertiesListItem
    name="components.Thread"
    type="(props: ThreadProps) => ReactNode"
  >
    Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread)
    component.
  </PropertiesListItem>
</PropertiesList>

### AnchoredThreads

Displays a list of [`Thread`][] components vertically alongside the editor.

```tsx highlight="3"
<LexicalComposer initialConfig={initialConfig}>
  <LiveblocksPlugin>
    <AnchoredThreads threads={threads} />
  </LiveblocksPlugin>
</LexicalComposer>
```

<Figure>
  <Image
    src="/assets/text-editor/anchored-threads.jpg"
    alt="AnchoredThreads"
    width={960}
    height={558}
  />
</Figure>

Takes a list of threads retrieved from [`useThreads`][] and renders them to the
page. Each thread is displayed at the same vertical coordinates as its
corresponding text highlight. If multiple highlights are in the same location,
each thread is placed in order below the previous thread.

```tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { useThreads } from "@liveblocks/react/suspense";
import {
  liveblocksConfig,
  LiveblocksPlugin,
  FloatingComposer,
  // +++
  AnchoredThreads,
  // +++
} from "@liveblocks/react-lexical";

const initialConfig = liveblocksConfig({
  namespace: "MyEditor",
  theme: {},
  nodes: [],
  onError: (err) => console.error(err),
});

function Editor() {
  // +++
  const { threads } = useThreads();
  // +++

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <LiveblocksPlugin>
        <FloatingComposer />
        // +++
        <AnchoredThreads threads={threads} style={{ width: "350px" }} />
        // +++
      </LiveblocksPlugin>
      <RichTextPlugin
        contentEditable={<ContentEditable />}
        placeholder={<div>Enter some text...</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
    </LexicalComposer>
  );
}
```

Should be nested inside [`LiveblocksPlugin`][].

<Banner title="Resolved threads">

The `AnchoredThreads` component automatically excludes resolved threads from
display. Any resolved threads passed in the threads list will not be shown.

</Banner>

#### Recommended usage [#AnchoredThreads-recommended-usage]

[`FloatingThreads`][] and [`AnchoredThreads`][] have been designed to work
together to provide the optimal experience on mobile and desktop. We generally
recommend using both components, hiding one on smaller screens, as we are below
with Tailwind classes. Most apps also don’t need to display resolved threads, so
we can filter those out with a [`useThreads`][] option.

```tsx
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-lexical";

function ThreadOverlay() {
  const { threads } = useThreads({ query: { resolved: false } });

  return (
    <>
      <FloatingThreads
        threads={threads}
        className="w-[350px] block md:hidden"
      />
      <AnchoredThreads
        threads={threads}
        className="w-[350px] hidden sm:block"
      />
    </>
  );
}
```

```tsx title="Alternatively use a media query hook" isCollapsed isCollapsable
import { useSyncExternalStore } from "react";
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-lexical";

function ThreadOverlay() {
  const { threads } = useThreads({ query: { resolved: false } });
  // +++
  const isMobile = useIsMobile();
  // +++

  // +++
  if (isMobile) {
    return <FloatingThreads threads={threads} style={{ width: "350px" }} />;
  }
  // +++

  // +++
  return <AnchoredThreads threads={threads} style={{ width: "350px" }} />;
  // +++
}

export function useIsMobile() {
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

function subscribe(callback: () => void) {
  const query = window.matchMedia("(max-width: 1024px)");

  query.addEventListener("change", callback);
  return () => query.removeEventListener("change", callback);
}

function getSnapshot() {
  const query = window.matchMedia("(max-width: 1024px)");
  return query.matches;
}
```

We can place this component inside [`ClientSideSuspense`][] to prevent it
rendering until threads have loaded.

```tsx
<LexicalComposer initialConfig={initialConfig}>
  <LiveblocksPlugin>
    <FloatingComposer />
    // +++
    <ClientSideSuspense fallback={null}>
      <ThreadOverlay />
    </ClientSideSuspense>
    // +++
  </LiveblocksPlugin>
</LexicalComposer>
```

#### Customization [#AnchoredThreads-customization]

The `AnchoredThreads` component acts as a wrapper around each [`Thread`][]. It
has no width, so setting this is required, and each thread will take on the
width of the wrapper. You can treat the component like you would a `div`, using
classes, listeners, and more.

```tsx
<AnchoredThreads
  threads={threads}
  style={{ width: "350px" }}
  className="my-anchored-thread"
/>
```

To apply styling to each [`Thread`][], you can pass a custom `Thread` property
to `components` and modify this in any way.

```tsx
import { Thread } from "@liveblocks/react-ui";

<AnchoredThreads
  threads={threads}
  style={{ width: "350px" }}
  className="my-anchored-thread"
  // +++
  components={{
    Thread: (props) => (
      <Thread
        {...props}
        className="border shadow"
        style={{ background: "white" }}
      />
    ),
  }}
  // +++
/>;
```

You can return any custom `ReactNode` here, including anything from a simple
wrapper around `Thread`, up to a full custom `Thread` component built using our
[Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment).

```tsx
import { Comment } from "@liveblocks/react-ui/primitives";

<AnchoredThreads
  threads={threads}
  style={{ width: "350px" }}
  className="my-anchored-thread"
  components={{
    Thread: (props) => (
      // +++
      <div>
        {props.thread.comments.map((comment) => (
          <Comment.Body
            key={comment.id}
            body={comment.body}
            components={/* ... */}
          />
        ))}
      </div>
      // +++
    ),
  }}
/>;
```

##### Modifying thread floating positions

Using CSS variables you can modify the gap between threads, and the horizontal
offset that’s added when a thread is selected.

```css
.lb-lexical-anchored-threads {
  /* Minimum gap between threads */
  --lb-lexical-anchored-threads-gap: 8px;

  /* How far the active thread is offset to the left */
  --lb-lexical-anchored-threads-active-thread-offset: 12px;
}
```

#### Props [#AnchoredThreads-props]

<PropertiesList>
  <PropertiesListItem name="threads" type="ThreadData[]" required>
    The threads to display.
  </PropertiesListItem>
  <PropertiesListItem
    name="components"
    type="Partial<AnchoredThreadsComponents>"
  >
    Override the component’s components.
  </PropertiesListItem>
  <PropertiesListItem
    name="components.Thread"
    type="(props: ThreadProps) => ReactNode"
  >
    Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread)
    component.
  </PropertiesListItem>
</PropertiesList>

### HistoryVersionPreview

The `HistoryVersionPreview` component allows you to display a preview of a
specific version of your Lexical editor's content. It also contains a button and
logic for restoring. It must be used inside the `<LiveblocksPlugin>` context. To
render a list of versions, see
[`VersionHistory`](/docs/api-reference/liveblocks-react-ui#Version-History).

#### Usage

```tsx
import { HistoryVersionPreview } from "@liveblocks/react-lexical";

function VersionPreview({ selectedVersion, onVersionRestore }) {
  return (
    <HistoryVersionPreview
      version={selectedVersion}
      onVersionRestore={onVersionRestore}
    />
  );
}
```

#### Props

<PropertiesList>
  <PropertiesListItem name="version" type="HistoryVersion" required>
    The version of the editor content to preview.
  </PropertiesListItem>
  <PropertiesListItem
    name="onVersionRestore"
    type="(version: HistoryVersion) => void"
  >
    Callback function called when the user chooses to restore this version.
  </PropertiesListItem>
</PropertiesList>

The `HistoryVersionPreview` component renders a read-only view of the specified
version of the editor content. It also provides a button for users to restore
the displayed version.

## Hooks

### useIsEditorReady

Used to check if the editor content has been loaded or not, helpful for
displaying a loading skeleton.

```ts
import { useIsEditorReady } from "@liveblocks/react-lexical";

const status = useIsEditorReady();
```

Here’s how it can be used in the context of your editor.

```tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import {
  liveblocksConfig,
  LiveblocksPlugin,
  // +++
  useIsEditorReady,
  // +++
} from "@liveblocks/react-lexical";

const initialConfig = liveblocksConfig({
  namespace: "MyEditor",
  theme: {},
  nodes: [],
  onError: (err) => console.error(err),
});

function Editor() {
  // +++
  const ready = useIsEditorReady();
  // +++

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <LiveblocksPlugin>
        <FloatingThreads />
        <FloatingComposer />
        <AnchoredThreads />
      </LiveblocksPlugin>
      // +++
      {!ready ? (
        <div>Loading...</div>
      ) : (
        <RichTextPlugin
          contentEditable={<ContentEditable />}
          placeholder={<div>Enter some text...</div>}
          ErrorBoundary={LexicalErrorBoundary}
        />
      )}
      // +++
    </LexicalComposer>
  );
}
```

### useIsThreadActive

Accepts a thread id and returns whether the thread annotation for this thread is
selected or not in the Lexical editor. This hook must be used in a component
nested inside [`LiveblocksPlugin`][].

```ts
import { useIsThreadActive } from "@liveblocks/react-lexical";

const isActive = useIsThreadActive(thread.id);
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="threadId" type="string" required>
    The ID of the thread.
  </PropertiesListItem>
</PropertiesList>

This hook can be useful to style threads based on whether their associated
thread annotations are selected or not in the editor.

## Utilities

### isTextFormatActive

Checks if a text format (bold, italic, etc.) is active in the current selection.
Takes a Lexical editor, and returns a `boolean`.

```tsx
import { isTextFormatActive } from "@liveblocks/react-lexical";

// "true" | "false"
const isActive = isTextFormatActive(editor, "bold");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="editor" type="LexicalEditor" required>
    The Lexical editor.
  </PropertiesListItem>
  <PropertiesListItem name="format" type="TextFormatType" required>
    The Lexical text format to check for in the current selection.
  </PropertiesListItem>
</PropertiesList>

#### Creating toggle buttons

The `isTextFormatActive` helper is particularly useful for creating buttons with
[`Toolbar.Toggle`][].

```tsx
import { FORMAT_TEXT_COMMAND } from "lexical";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
// +++
import { isTextFormatActive, Toolbar } from "@liveblocks/react-lexical";
// +++

function CustomToggleButton() {
  const [editor] = useLexicalComposerContext();

  return (
    <Toolbar.Toggle
      name="Bold"
      icon={<strong>B️</strong>}
      // +++
      active={isTextFormatActive(editor, "bold")}
      // +++
      onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")}
    />
  );
}
```

### isBlockNodeActive

Checks if a block node is active in the current selection. If the selection
contains multiple block nodes, it will only return `true` if all of them are of
the same type.

```tsx
import { isBlockNodeActive } from "@liveblocks/react-lexical";
import { $isTextNode } from "lexical";

// Checking if text node is currently active
const isActive = isBlockNodeActive(editor, $isTextNode);
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="editor" type="LexicalEditor" required>
    The Lexical editor.
  </PropertiesListItem>
  <PropertiesListItem
    name="isActive"
    type="(node: LexicalNode) => boolean"
    required
  >
    Function that passes the current node, helping you check if the current
    block node is active. Helpful in combination with `$is___Node` functions.
  </PropertiesListItem>
</PropertiesList>

#### Creating custom block selector items

The `isBlockNodeActive` helper is particularly useful for adding custom
[`Toolbar.BlockSelector`][] items.

```tsx
// +++
import { isBlockNodeActive, Toolbar } from "@liveblocks/react-lexical";
// +++
import { $setBlocksType } from "@lexical/selection";
import { $isCodeNode } from "@lexical/code";
import { $getSelection } from "lexical";

<Toolbar>
  <Toolbar.BlockSelector
    items={(defaultItems) => [
      ...defaultItems,
      {
        name: "Code block",
        icon: <div>❮ ❯</div>,
        // +++
        isActive: (editor) => isBlockNodeActive(editor, $isCodeNode),
        // +++
        setActive: (editor) =>
          $setBlocksType($getSelection(), () => $createCodeNode()),
      },
    ]}
  />
</Toolbar>;
```

## Stylesheets

React Lexical comes with default styles, and these can be imported into the root
of your app or directly into a CSS file with `@import`. Note that you must also
install and import a stylesheet from
[`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui) to use these
styles.

```tsx
import "@liveblocks/react-ui/styles.css";
import "@liveblocks/react-lexical/styles.css";
```

### Customizing your styles

Adding dark mode and customizing your styles is part of `@liveblocks/react-ui`,
learn how to do this under
[styling and customization](/docs/api-reference/liveblocks-react-ui#Styling-and-customization).

## Deprecated

### useEditorStatus

<Banner title="Deprecated">

This is no longer supported. Starting with 2.12.0, we recommend using
[`useSyncStatus`][] instead for tracking sync status, because it will reflect
sync status of all parts of Liveblocks, not just Storage.

</Banner>

Returns the current editor status.

```ts
import { useEditorStatus } from "@liveblocks/react-lexical";

const status = useEditorStatus();
```

The possible values are:

- `not-loaded`: Initial editor state when entering the room.
- `loading`: Once the editor state has been requested by `LiveblocksPlugin`.
- `synchronized`: The editor state is sync with Liveblocks servers.

[`LiveblocksPlugin`]: #LiveblocksPlugin
[`LexicalComposer`]: https://lexical.dev/docs/react/plugins
[`Thread`]: /docs/api-reference/liveblocks-react-ui#Thread
[`Composer`]: /docs/api-reference/liveblocks-react-ui#Composer
[`useThreads`]: /docs/api-reference/liveblocks-react#useThreads
[`Icon`]: /docs/api-reference/liveblocks-react-ui#Icon
[`Toolbar`]: #Toolbar
[`Toolbar.Button`]: #Toolbar.Button
[`Toolbar.Toggle`]: #Toolbar.Toggle
[`Toolbar.BlockSelector`]: #Toolbar.BlockSelector
[`Toolbar.Separator`]: #Toolbar.Separator
[`Toolbar.SectionHistory`]: #Toolbar.SectionHistory
[`Toolbar.SectionInline`]: #Toolbar.SectionInline
[`Toolbar.SectionCollaboration`]: #Toolbar.SectionCollaboration
[`FloatingToolbar`]: #FloatingToolbar
[`FloatingComposer`]: #FloatingComposer
[`FloatingThreads`]: #FloatingThreads
[`AnchoredThreads`]: #AnchoredThreads
[`ClientSideSuspense`]: /docs/api-reference/liveblocks-react#ClientSideSuspense
[`useSyncStatus`]: /docs/api-reference/liveblocks-react#useSyncStatus

---
meta:
  title: "@liveblocks/react-tiptap"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/react-tiptap package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/react-tiptap` provides you with a [React](https://react.dev/)
plugin that adds collaboration to any [Tiptap](https://tiptap.dev/) text editor.
It also adds realtime cursors, document persistence on the cloud, comments, and
mentions. Read our [get started guides](/docs/get-started/text-editor/tiptap) to
learn more. Use
[`@liveblocks/node-prosemirror`](/docs/api-reference/liveblocks-node-prosemirror)
for server-side editing.

## Setup

To set up your collaborative Tiptap editor, add
[`useLiveblocksExtension`](#useLiveblocksExtension) to your editor, passing the
return value `useEditor` extension array.

```tsx
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  // +++
  const liveblocks = useLiveblocksExtension();
  // +++

  const editor = useEditor({
    extensions: [
      // +++
      liveblocks,
      // +++
      // ...
    ],
  });

  return (
    <div>
      <EditorContent editor={editor} />
    </div>
  );
}
```

Liveblocks Tiptap components should be passed `editor` to enable them.

```tsx
import {
  useLiveblocksExtension,
  // +++
  FloatingComposer,
  // +++
} from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension();

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  return (
    <div>
      <EditorContent editor={editor} />
      // +++
      <FloatingComposer editor={editor} style={{ width: "350px" }} />
      // +++
    </div>
  );
}
```

Learn more in our [get started guides](/docs/get-started/text-editor/tiptap).

## Default components

### Toolbar

Displays a toolbar, allowing you to change the styles of selected text. You can
add content [before or after](#toolbar-extending-the-defaults), or the toolbar’s
options can be [customized](#creating-a-custom-toolbar). A
[floating toolbar](#FloatingToolbar) also exists.

```tsx
<Toolbar editor={editor} />
```

<Figure>
  <Image
    src="/assets/text-editor/toolbar.png"
    alt="Toolbar"
    width={960}
    height={558}
  />
</Figure>

Pass your Tiptap `editor` to use the component. By default, one of the toolbar
buttons can create comment threads—to enable this add [`FloatingComposer`][] and
display threads with [`AnchoredThreads`][] or [`FloatingThreads`][].

```tsx
import {
  useLiveblocksExtension,
  // +++
  Toolbar,
  // +++
  FloatingComposer,
  FloatingThreads,
} from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension();

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  return (
    <div>
      // +++
      <Toolbar editor={editor} />
      // +++
      <EditorContent editor={editor} />
      <FloatingComposer editor={editor} style={{ width: "350px" }} />
      <FloatingThreads editor={editor} style={{ width: "350px" }} />
    </div>
  );
}
```

#### Extending the defaults [#toolbar-extending-the-defaults]

You can insert content `before` the first button and `after` the last button
using `before` and `after`. Components such as [`Toolbar.Button`][] and
[`Toolbar.Toggle`][] can be used to create new buttons.

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";
import { Icon } from "@liveblocks/react-ui";

<Toolbar
  editor={editor}
  // +++
  before={<>I'm at the start</>}
  after={
    <Toolbar.Button
      name="Help"
      icon={<Icon.QuestionMark />}
      shortcut="CMD-H"
      onClick={() => console.log("help")}
    />
  }
  // +++
/>;
```

For more complex customization, instead read
[creating a custom floating toolbar](#creating-a-custom-toolbar).

#### Creating a custom toolbar [#creating-a-custom-toolbar]

By passing elements as children, it’s possible to create a fully custom toolbar.

```tsx highlight="7"
import { Toolbar } from "@liveblocks/react-tiptap";
import { Editor } from "@tiptap/react";

function CustomToolbar({ editor }: { editor: Editor | null }) {
  return (
    <Toolbar editor={editor}>
      Hello <strong>world</strong>
    </Toolbar>
  );
}
```

Each part of our default toolbar is available as blocks which can be slotted
together. This is how the default toolbar is constructed:

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";
import { Editor } from "@tiptap/react";

function CustomToolbar({ editor }: { editor: Editor | null }) {
  return (
    <Toolbar editor={editor}>
      // +++
      <Toolbar.BlockSelector />
      <Toolbar.SectionInline />
      <Toolbar.Separator />
      <Toolbar.SectionCollaboration />
      // +++
    </Toolbar>
  );
}
```

You can mix these default components with any custom ones of your own. Below the
[`Toolbar.SectionHistory`][] component is added alongside some custom buttons
created with [`Toolbar.Button`][], [`Toolbar.Toggle`][], and [`Icon`][]. The
highlight toggle button requires a
[Tiptap extension](https://tiptap.dev/docs/editor/extensions/marks/highlight).

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";
import { Icon } from "@liveblocks/react-ui";
import { Editor } from "@tiptap/react";

function CustomToolbar({ editor }: { editor: Editor | null }) {
  return (
    <Toolbar editor={editor}>
      // +++
      <Toolbar.SectionHistory />
      <Toolbar.Separator />
      <Toolbar.Button
        name="Help"
        icon={<Icon.QuestionMark />}
        shortcut="CMD-H"
        onClick={() => console.log("help")}
      />
      <Toolbar.Toggle
        name="Highlight"
        icon={<div>🖊️</div>}
        active={editor?.isActive("highlight") ?? false}
        onClick={() => editor?.chain().focus().toggleHighlight().run()}
        disabled={!editor?.can().chain().focus().toggleHighlight().run()}
      />
      // +++
    </Toolbar>
  );
}
```

To learn more about the different components, read more below.

#### Props [#Toolbar-props]

<PropertiesList>
  <PropertiesListItem name="editor" type="Editor | null" required>
    The Tiptap editor.
  </PropertiesListItem>
  <PropertiesListItem name="children" type="ReactNode">
    The content of the toolbar, overriding the default content. Use the `before`
    and `after` props if you want to keep and extend the default content. Any
    `ReactNode` or `Toolbar.*` components work inside.
  </PropertiesListItem>
  <PropertiesListItem name="before" type="ReactNode">
    The content to display at the start of the toolbar. Any `ReactNode` or
    `Toolbar.*` components work inside.
  </PropertiesListItem>
  <PropertiesListItem name="after" type="ReactNode">
    The content to display at the end of the toolbar. Any `ReactNode` or
    `Toolbar.*` components work inside.
  </PropertiesListItem>
</PropertiesList>

#### Toolbar.Button

A button for triggering actions. The `name` is displayed in a tooltip. Props
such as `onClick` will be passed to the underlying `button` element.

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";

<Toolbar editor={editor}>
  <Toolbar.Button name="Question" onClick={(e) => console.log("Clicked")} />
</Toolbar>;
```

Optionally takes an icon which will visually replace the `name`. Also optionally
accepts a shortcut, which is displayed in the tooltip. Comment key names are
converted to symbols. Here are various examples.

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";
import { Icon } from "@liveblocks/react-ui";

// Button says "Question"
<Toolbar.Toggle name="Question" onClick={/* ... */} />

// Tooltip says "Question [⌘+Q]"
<Toolbar.Button name="Question" shortcut="CMD+Q" onClick={/* ... */} />

// Custom icon, replaces the name in the button
<Toolbar.Button name="Question" icon={<div>?</div>} onClick={/* ... */} />

// Using a Liveblocks icon, replaces the name in the button
<Toolbar.Button name="Question" icon={<Icon.QuestionMark />} onClick={/* ... */} />

// Passing children visually replaces the `name` and `icon`
<Toolbar.Button name="Question" onClick={/* ... */}>
  ? Ask a question
</Toolbar.Button>

// Props are passed to the inner `button`
<Toolbar.Button
  name="Question"
  style={{ marginLeft: 10 }}
  className="custom-button"
  onMouseOver={() => console.log("Hovered")}
/>
```

##### Props [#ToolbarButton-props]

<PropertiesList>
  <PropertiesListItem name="name" type="string" required>
    The name of this button displayed in its tooltip. Will also be displayed in
    the button if no `icon` or `children` are passed.
  </PropertiesListItem>
  <PropertiesListItem name="icon" type="ReactNode">
    An optional icon displayed in this button.
  </PropertiesListItem>
  <PropertiesListItem name="shortcut" type="string">
    An optional keyboard shortcut displayed in this button’s tooltip. Common
    shortcuts such will be replaced by their symbols, for example `CMD` → `⌘`.
  </PropertiesListItem>
</PropertiesList>

#### Toolbar.Toggle

A toggle button for values that can be active or inactive. Best used with text
editor commands. The `name` is displayed in a tooltip. Props will be passed to
the underlying `button` element.

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";

<Toolbar editor={editor}>
  <Toolbar.Toggle
    name="Highlight"
    active={editor?.isActive("highlight") ?? false}
    onClick={() => editor?.chain().focus().toggleHighlight().run()}
  />
</Toolbar>;
```

The snippet above shows how to use the Toggle with the
[Tiptap highlight extension](https://tiptap.dev/docs/editor/extensions/marks/highlight).
The toggle button can also be toggled with `useState`.

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";
import { Editor } from "@tiptap/react";
import { useState } from "react";

function CustomToggle({ editor }: { editor: Editor | null }) {
  const [active, setActive] = useState(false);

  return (
    <Toolbar.Toggle
      name="Toggle options"
      active={active}
      onClick={() => setActive(!active)}
    />
  );
}
```

`Toolbar.Toggle` optionally takes an icon which will visually replace the
`name`. Also optionally accepts a shortcut, which is displayed in the tooltip.
Comment key names are converted to symbols. Here are various examples.

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";
import { Icon } from "@liveblocks/react-ui";

// Button says "Highlight"
<Toolbar.Toggle
  name="Highlight"
  active={/* ... */}
  onClick={/* ... */}
/>

// Tooltip says "Highlight [⌘+H]"
<Toolbar.Toggle
  name="Highlight"
  shortcut="CMD+H"
  active={/* ... */}
  onClick={/* ... */}
/>

// Custom icon, replaces the name in the button
<Toolbar.Toggle
  name="Highlight"
  icon={<div>🖊</div>}
  active={/* ... */}
  onClick={/* ... */}
/>

// Using a Liveblocks icon, replaces the name in the button
<Toolbar.Toggle
  name="Highlight"
  icon={<Icon.QuestionMark />}
  active={/* ... */}
  onClick={/* ... */}
/>

// Passing children visually replaces the `name` and `icon`
<Toolbar.Toggle
  name="Highlight"
  active={/* ... */}
  onClick={/* ... */}
>
  🖊️Highlight
</Toolbar.Toggle>

// Props are passed to the inner `button`
<Toolbar.Toggle
  name="Highlight"
  active={/* ... */}
  onClick={/* ... */}
  style={{ marginLeft: 10 }}
  className="custom-toggle"
  onMouseOver={() => console.log("Hovered")}
/>
```

##### Props [#ToolbarToggle-props]

<PropertiesList>
  <PropertiesListItem name="name" type="string" required>
    The name of this button displayed in its tooltip. Will also be displayed in
    the button if no `icon` or `children` are passed.
  </PropertiesListItem>
  <PropertiesListItem name="active" type="boolean" required>
    Whether the button is toggled.
  </PropertiesListItem>
  <PropertiesListItem name="icon" type="ReactNode">
    An optional icon displayed in this button.
  </PropertiesListItem>
  <PropertiesListItem name="shortcut" type="string">
    An optional keyboard shortcut displayed in this button’s tooltip. Common
    shortcuts such will be replaced by their symbols, for example `CMD` → `⌘`.
  </PropertiesListItem>
</PropertiesList>

#### Toolbar.BlockSelector

Adds a dropdown selector for switching between different block types, such as
_text_, _heading 1_, _blockquote_. Props will be passed to the inner `button`
element. Can also be placed inside [`FloatingToolbar`][].

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";

<Toolbar editor={editor}>
  <Toolbar.BlockSelector />
</Toolbar>;
```

##### Use custom item options

If you’d like to change the items shown in the dropdown menu, you can pass a
custom `items` array. Below a code block item
([Tiptap extension](https://tiptap.dev/docs/editor/extensions/nodes/code-block))
is added after the default options.

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";

<Toolbar editor={editor}>
  <Toolbar.BlockSelector
    items={(defaultItems) => [
      ...defaultItems,
      {
        name: "Code block",
        icon: <div>❮ ❯</div>, // Optional
        isActive: (editor) => editor.isActive("codeBlock"),
        setActive: (editor) =>
          editor.chain().focus().clearNodes().toggleCodeBlock().run(),
      },
    ]}
  />
</Toolbar>;
```

##### Customize item styles

By passing a `label` property, you can overwrite the styles of the dropdown
items. The toolbar button will still display the `name`, but in the dropdown,
the `label` will be used instead of the `name` and `icon`. Below, a new item is
added and its `label` is customized.

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";

<Toolbar editor={editor}>
  <Toolbar.BlockSelector
    items={(defaultItems) => [
      ...defaultItems,
      {
        name: "Code block",
        // +++
        label: <div className="font-mono">Code</div>, // Optional, overwrites `icon` + `name`
        // +++
        isActive: (editor) => editor.isActive("codeBlock"),
        setActive: (editor) =>
          editor.chain().focus().clearNodes().toggleCodeBlock().run(),
      },
    ]}
  />
</Toolbar>;
```

You can also customize the default items. Below each item is styled to represent
the effect each block applies to the document.

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";

<Toolbar.BlockSelector
  items={(defaultItems) =>
    defaultItems.map((item) => {
      let label;

      if (item.name === "Text") {
        label = <span>Regular text</span>;
      }

      if (item.name === "Heading 1") {
        label = (
          <span style={{ fontSize: 18, fontWeight: "bold" }}>Heading 1</span>
        );
      }

      if (item.name === "Heading 2") {
        label = (
          <span style={{ fontSize: 16, fontWeight: "bold" }}>Heading 2</span>
        );
      }

      if (item.name === "Heading 3") {
        label = (
          <span style={{ fontSize: 15, fontWeight: "bold" }}>Heading 3</span>
        );
      }

      if (item.name === "Blockquote") {
        label = (
          <span style={{ borderLeft: "3px solid gray", paddingLeft: 8 }}>
            Blockquote
          </span>
        );
      }

      return {
        ...item,
        label,
        icon: null, // Hide all icons
      };
    })
  }
/>;
```

##### Props [#ToolbarBlockSelector-props]

<PropertiesList>
  <PropertiesListItem
    name="items"
    type="array | function"
    detailedType="ToolbarBlockSelectorItem[] | (defaultItems: ToolbarBlockSelectorItem[]) => ToolbarBlockSelectorItem[]"
  >
    The items displayed in this block selector. When provided as an array, the
    default items are overridden. To avoid this, a function can be provided
    instead and it will receive the default items.
  </PropertiesListItem>
</PropertiesList>

#### Toolbar.Separator

Adds a visual, and accessible, separator used to separate sections in the
toolbar. Props will be passed to the inner `div` element. Can also be placed
inside [`FloatingToolbar`][].

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";

<Toolbar editor={editor}>
  // +++
  <Toolbar.SectionHistory />
  // +++
</Toolbar>;
```

#### Toolbar.SectionHistory

Adds a section containing _undo_ and _redo_ buttons. Can also be placed inside
[`FloatingToolbar`][].

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";

<Toolbar editor={editor}>
  // +++
  <Toolbar.SectionHistory />
  // +++
</Toolbar>;
```

#### Toolbar.SectionInline

Adds a section containing inline formatting actions such as _bold_, _italic_,
_underline_. Can also be placed inside [`FloatingToolbar`][].

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";

<Toolbar editor={editor}>
  // +++
  <Toolbar.SectionInline />
  // +++
</Toolbar>;
```

#### Toolbar.SectionCollaboration

Adds a section containing an _add comment_ button. Can also be placed inside
[`FloatingToolbar`][].

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";

<Toolbar editor={editor}>
  // +++
  <Toolbar.SectionCollaboration />
  // +++
</Toolbar>;
```

### FloatingToolbar

Displays a floating toolbar near the current Tiptap selection, allowing you to
change styles. You can add content
[before or after](#floating-toolbar-extending-the-defaults), or the toolbar’s
options can be [customized](#creating-a-custom-floating-toolbar). A
[static toolbar](#Toolbar) also exists.

```tsx
<FloatingToolbar editor={editor} />
```

<Figure>
  <Image
    src="/assets/text-editor/floating-toolbar.png"
    alt="FloatingToolbar"
    width={960}
    height={558}
  />
</Figure>

Pass your Tiptap `editor` to use the component. By default, one of the toolbar
buttons can create comment threads—to enable this add [`FloatingComposer`][] and
display threads with [`AnchoredThreads`][] or [`FloatingThreads`][].

```tsx
import {
  useLiveblocksExtension,
  // +++
  FloatingToolbar,
  // +++
  FloatingComposer,
  FloatingThreads,
} from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension();

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  return (
    <div>
      <EditorContent editor={editor} />
      // +++
      <FloatingToolbar editor={editor} />
      // +++
      <FloatingComposer editor={editor} style={{ width: "350px" }} />
      <FloatingThreads editor={editor} style={{ width: "350px" }} />
    </div>
  );
}
```

#### Changing float position

Using `position` and `offset` you can reposition the toolbar relative to the
current selection. `position` can be set to `"top"` or `"bottom"`, and `offset`
defines the vertical distance in pixels from the selection.

```tsx
<FloatingToolbar
  editor={editor}
  // +++
  position="bottom" // Position can be `top` or `bottom`
  offset={12} // Distance in px from selection
  // +++
/>
```

#### Extending the defaults [#floating-toolbar-extending-the-default]

You can insert custom content `before` the first button and `after` the last
button using `before` and `after`. Components such as [`Toolbar.Button`][] and
[`Toolbar.Toggle`][] can be used to create new buttons.

```tsx
import { Toolbar } from "@liveblocks/react-tiptap";
import { Icon } from "@liveblocks/react-ui";

<FloatingToolbar
  editor={editor}
  // +++
  before={<>I'm at the start</>}
  after={
    <Toolbar.Button
      name="Help"
      icon={<Icon.QuestionMark />}
      shortcut="CMD-H"
      onClick={() => console.log("help")}
    />
  }
  // +++
/>;
```

For more complex customization, instead read
[creating a custom floating toolbar](#creating-a-custom-floating-toolbar).

#### Creating a custom floating toolbar [#creating-a-custom-floating-toolbar]

By passing elements as children, it’s possible to create a fully custom floating
toolbar.

```tsx highlight="7"
import { FloatingToolbar } from "@liveblocks/react-tiptap";
import { Editor } from "@tiptap/react";

function CustomToolbar({ editor }: { editor: Editor | null }) {
  return (
    <FloatingToolbar editor={editor}>
      Hello <strong>world</strong>
    </FloatingToolbar>
  );
}
```

Each part of our default toolbar is available as blocks which can be slotted
together. This is how the default floating toolbar is constructed:

```tsx
import { FloatingToolbar, Toolbar } from "@liveblocks/react-tiptap";
import { Editor } from "@tiptap/react";

function CustomToolbar({ editor }: { editor: Editor | null }) {
  return (
    <FloatingToolbar editor={editor}>
      // +++
      <Toolbar.BlockSelector />
      <Toolbar.SectionInline />
      <Toolbar.Separator />
      <Toolbar.SectionCollaboration />
      // +++
    </FloatingToolbar>
  );
}
```

You can mix these default components with any custom ones of your own. Below the
[`Toolbar.SectionHistory`][] component is added alongside some custom buttons
created with [`Toolbar.Button`][], [`Toolbar.Toggle`][], and [`Icon`][]. The
highlight toggle button requires a
[Tiptap extension](https://tiptap.dev/docs/editor/extensions/marks/highlight).

```tsx
import { FloatingToolbar, Toolbar } from "@liveblocks/react-tiptap";
import { Icon } from "@liveblocks/react-ui";
import { Editor } from "@tiptap/react";

function CustomToolbar({ editor }: { editor: Editor | null }) {
  return (
    <FloatingToolbar editor={editor}>
      // +++
      <Toolbar.SectionHistory />
      <Toolbar.Separator />
      <Toolbar.Button
        name="Help"
        icon={<Icon.QuestionMark />}
        shortcut="CMD-H"
        onClick={() => console.log("help")}
      />
      <Toolbar.Toggle
        name="Highlight"
        icon={<div>🖊️</div>}
        active={editor?.isActive("highlight") ?? false}
        onClick={() => editor?.chain().focus().toggleHighlight().run()}
        disabled={!editor?.can().chain().focus().toggleHighlight().run()}
      />
      // +++
    </FloatingToolbar>
  );
}
```

To learn more about the different components, read more under [`Toolbar`][].

#### Props [#FloatingToolbar-props]

<PropertiesList>
  <PropertiesListItem name="editor" type="Editor | null" required>
    The Tiptap editor.
  </PropertiesListItem>
  <PropertiesListItem name="position" type={`"top" | "bottom"`}>
    The vertical position of the floating toolbar.
  </PropertiesListItem>
  <PropertiesListItem name="offset" type="number">
    The vertical offset of the floating toolbar from the selection.
  </PropertiesListItem>
  <PropertiesListItem name="children" type="ReactNode">
    The content of the toolbar, overriding the default content. Use the `before`
    and `after` props if you want to keep and extend the default content. Any
    `ReactNode` or `Toolbar.*` components work inside.
  </PropertiesListItem>
  <PropertiesListItem name="before" type="ReactNode">
    The content to display at the start of the toolbar. Any `ReactNode` or
    `Toolbar.*` components work inside.
  </PropertiesListItem>
  <PropertiesListItem name="after" type="ReactNode">
    The content to display at the end of the toolbar. Any `ReactNode` or
    `Toolbar.*` components work inside.
  </PropertiesListItem>
</PropertiesList>

### FloatingComposer

Displays a [`Composer`][] near the current Tiptap selection, allowing you to
create threads.

```tsx highlight="3"
<FloatingComposer editor={editor} />
```

<Figure>
  <Image
    src="/assets/text-editor/floating-composer.jpg"
    alt="FloatingComposer"
    width={960}
    height={558}
  />
</Figure>

Submitting a comment will attach an annotation thread at the current selection.
Should be passed your Tiptap `editor`, and it’s recommended you set a width
value. Display created threads with [`AnchoredThreads`][] or
[`FloatingThreads`][].

```tsx
import {
  useLiveblocksExtension,
  // +++
  FloatingComposer,
  // +++
  FloatingThreads,
} from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension();

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  return (
    <div>
      <EditorContent editor={editor} />
      // +++
      <FloatingComposer editor={editor} style={{ width: "350px" }} />
      // +++
      <FloatingThreads editor={editor} style={{ width: "350px" }} />
    </div>
  );
}
```

#### Opening the composer

To open the `FloatingComposer`, you need to click the “Comment” button in the
[`Toolbar`][] or call the `addPendingComment`
[command](https://tiptap.dev/docs/editor/api/commands) added by Liveblocks. You
can use `liveblocksCommentMark` to check if the current selection is a comment.

```tsx
import { Editor } from "@tiptap/react";

function Toolbar({ editor }: { editor: Editor | null }) {
  if (!editor) {
    return null;
  }

  return (
    <button
      onClick={() => {
        // +++
        editor.chain().focus().addPendingComment().run();
        // +++
      }}
      // +++
      data-active={editor.isActive("liveblocksCommentMark")}
      // +++
    >
      💬 New comment
    </button>
  );
}
```

#### Customization [#FloatingComposer-customization]

The `FloatingComposer` component acts as a wrapper around a [`Composer`][], near
the current selection. You can treat the component like you would a `form`,
using classes, listeners, and more.

```tsx
<FloatingComposer
  editor={editor}
  style={{ width: "350px" }}
  className="my-floating-composer"
/>
```

To apply styling to the composer, you can pass a custom `Composer` property to
`components` and modify this in any way.

```tsx
import { Composer } from "@liveblocks/react-ui";

<FloatingComposer
  editor={editor}
  style={{ width: "350px" }}
  // +++
  components={{
    Composer: (props) => (
      <Composer
        {...props}
        className="border shadow"
        style={{ width: "300px" }}
      />
    ),
  }}
  // +++
/>;
```

You can return any custom `ReactNode` here, including anything from a simple
wrapper around `Composer`, up to a full custom `Composer` component built using
our
[Composer primitives](/docs/api-reference/liveblocks-react-ui#primitives-Composer).

```tsx
import { Composer } from "@liveblocks/react-ui/primitives";

<FloatingComposer
  editor={editor}
  style={{ width: "350px" }}
  components={{
    Composer: ({ onComposerSubmit }) => (
      // +++
      <Composer.Form onComposerSubmit={onComposerSubmit}>
        <Composer.Editor />
        <Composer.Submit>Send</Composer.Submit>
      </Composer.Form>
      // +++
    ),
  }}
/>;
```

You can also customize submission behavior by passing a custom
`onComposerSubmit` function to the `Composer.Form` component.

```tsx
import { Composer } from "@liveblocks/react-ui/primitives";

<FloatingComposer
  editor={editor}
  style={{ width: "350px" }}
  components={{
    Composer: () => (
      <Composer.Form
        // +++
          onComposerSubmit={(comment, event) => {
            event.preventDefault();

            const thread = createThread({
              body: comment.body,
              attachments: comment.attachments,
              metadata: ...,
            });

            editor.commands.addComment(thread.id);
          }}
          // +++
        >
          <Composer.Editor />
          <Composer.Submit>Send</Composer.Submit>
        </Composer.Form>
      ),
  }}
/>;
```

#### Props [#FloatingComposer-props]

<PropertiesList>
  <PropertiesListItem name="metadata" type="ThreadMetadata">
    The metadata of the thread to create.
  </PropertiesListItem>
  <PropertiesListItem name="commentMetadata" type="CommentMetadata">
    The metadata of the comment to create.
  </PropertiesListItem>
  <PropertiesListItem
    name="onComposerSubmit"
    type="function"
    detailedType="(comment: ComposerSubmitComment, event: FormEvent<HTMLFormElement>) => Promise<void> | void"
  >
    The event handler called when the composer is submitted.
  </PropertiesListItem>
  <PropertiesListItem name="defaultValue" type="CommentBody">
    The composer’s initial value.
  </PropertiesListItem>
  <PropertiesListItem name="collapsed" type="boolean">
    Whether the composer is collapsed. Setting a value will make the composer
    controlled.
  </PropertiesListItem>
  <PropertiesListItem
    name="onCollapsedChange"
    type="function"
    detailedType="(collapsed: boolean) => void"
  >
    The event handler called when the collapsed state of the composer changes.
  </PropertiesListItem>
  <PropertiesListItem name="defaultCollapsed" type="boolean">
    Whether the composer is initially collapsed. Setting a value will make the
    composer uncontrolled.
  </PropertiesListItem>
  <PropertiesListItem name="disabled" type="boolean">
    Whether the composer is disabled.
  </PropertiesListItem>
  <PropertiesListItem name="autoFocus" type="boolean">
    Whether to focus the composer on mount.
  </PropertiesListItem>
  <PropertiesListItem
    name="overrides"
    type="Partial<GlobalOverrides & ComposerOverrides>"
  >
    Override the component’s strings.
  </PropertiesListItem>
  <PropertiesListItem
    name="components"
    type="Partial<FloatingComposerComponents>"
  >
    Override the component’s components.
  </PropertiesListItem>
  <PropertiesListItem
    name="components.Composer"
    type="(props: ComposerProps) => ReactNode"
  >
    Override the [`Composer`](/docs/api-reference/liveblocks-react-ui#Composer)
    component.
  </PropertiesListItem>
</PropertiesList>

### FloatingThreads

Displays floating [`Thread`][] components below text highlights in the editor.

```tsx highlight="3"
<FloatingThreads editor={editor} threads={threads} />
```

<Figure>
  <Image
    src="/assets/text-editor/floating-threads.jpg"
    alt="FloatingThreads"
    width={960}
    height={558}
  />
</Figure>

Takes a list of threads retrieved from [`useThreads`][] and renders them to the
page. Each thread is opened by clicking on its corresponding text highlight.
Should be passed your Tiptap `editor`, and it’s recommended you set a width
value.

```tsx
// +++
import { useThreads } from "@liveblocks/react/suspense";
// +++
import {
  useLiveblocksExtension,
  FloatingComposer,
  // +++
  FloatingThreads,
  // +++
} from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension();

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  // +++
  const { threads } = useThreads();
  // +++

  return (
    <div>
      <EditorContent editor={editor} />
      <FloatingComposer editor={editor} style={{ width: "350px" }} />
      // +++
      <FloatingThreads
        editor={editor}
        threads={threads}
        style={{ width: "350px" }}
      />
      // +++
    </div>
  );
}
```

<Banner title="Resolved threads">

The `FloatingThreads` component automatically excludes resolved threads from
display. Any resolved threads passed in the threads list will not be shown.

</Banner>

#### Recommended usage [#FloatingThreads-recommended-usage]

[`FloatingThreads`][] and [`AnchoredThreads`][] have been designed to work
together to provide the optimal experience on mobile and desktop. We generally
recommend using both components, hiding one on smaller screens, as we are below
with Tailwind classes. Most apps also don’t need to display resolved threads, so
we can filter those out with a [`useThreads`][] option.

```tsx
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-tiptap";
import { Editor } from "@tiptap/react";

function ThreadOverlay({ editor }: { editor: Editor | null }) {
  const { threads } = useThreads({ query: { resolved: false } });

  return (
    <>
      <FloatingThreads
        editor={editor}
        threads={threads}
        className="w-[350px] block md:hidden"
      />
      <AnchoredThreads
        editor={editor}
        threads={threads}
        className="w-[350px] hidden sm:block"
      />
    </>
  );
}
```

```tsx title="Alternatively use a media query hook" isCollapsed isCollapsable
import { useSyncExternalStore } from "react";
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-tiptap";
import { Editor } from "@tiptap/react";

function ThreadOverlay({ editor }: { editor: Editor | null }) {
  const { threads } = useThreads({ query: { resolved: false } });
  // +++
  const isMobile = useIsMobile();
  // +++

  // +++
  if (isMobile) {
    return (
      <FloatingThreads
        editor={editor}
        threads={threads}
        style={{ width: "350px" }}
      />
    );
  }
  // +++

  // +++
  return (
    <AnchoredThreads
      editor={editor}
      threads={threads}
      style={{ width: "350px" }}
    />
  );
  // +++
}

export function useIsMobile() {
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

function subscribe(callback: () => void) {
  const query = window.matchMedia("(max-width: 1024px)");

  query.addEventListener("change", callback);
  return () => query.removeEventListener("change", callback);
}

function getSnapshot() {
  const query = window.matchMedia("(max-width: 1024px)");
  return query.matches;
}
```

We can place this component inside [`ClientSideSuspense`][] to prevent it
rendering until threads have loaded.

```tsx
<div>
  <EditorContent editor={editor} />
  <FloatingComposer editor={editor} style={{ width: "350px" }} />
  // +++
  <ClientSideSuspense fallback={null}>
    <ThreadOverlay editor={editor} />
  </ClientSideSuspense>
  // +++
</div>
```

#### Customization [#FloatingThreads-customization]

The `FloatingThreads` component acts as a wrapper around each individual
[`Thread`][]. You can treat the component like you would a `div`, using classes,
listeners, and more.

```tsx
<FloatingThreads
  editor={editor}
  threads={threads}
  className="my-floating-thread"
/>
```

To apply styling to each [`Thread`][], you can pass a custom `Thread` property
to `components` and modify this in any way. This is the best way to modify a
thread’s width.

```tsx
import { Thread } from "@liveblocks/react-ui";

<FloatingThreads
  editor={editor}
  threads={threads}
  className="my-floating-thread"
  // +++
  components={{
    Thread: (props) => (
      <Thread {...props} className="border shadow" style={{ width: "300px" }} />
    ),
  }}
  // +++
/>;
```

You can return any custom `ReactNode` here, including anything from a simple
wrapper around `Thread`, up to a full custom `Thread` component built using our
[Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment).

```tsx
import { Comment } from "@liveblocks/react-ui/primitives";

<FloatingThreads
  editor={editor}
  threads={threads}
  className="my-floating-thread"
  components={{
    Thread: (props) => (
      // +++
      <div>
        {props.thread.comments.map((comment) => (
          <Comment.Body
            key={comment.id}
            body={comment.body}
            components={/* ... */}
          />
        ))}
      </div>
      // +++
    ),
  }}
/>;
```

#### Props [#FloatingThreads-props]

<PropertiesList>
  <PropertiesListItem name="threads" type="ThreadData[]" required>
    The threads to display.
  </PropertiesListItem>
  <PropertiesListItem
    name="components"
    type="Partial<AnchoredThreadsComponents>"
  >
    Override the component’s components.
  </PropertiesListItem>
  <PropertiesListItem
    name="components.Thread"
    type="(props: ThreadProps) => ReactNode"
  >
    Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread)
    component.
  </PropertiesListItem>
</PropertiesList>

### AnchoredThreads

Displays a list of [`Thread`][] components vertically alongside the editor.

```tsx highlight="3"
<AnchoredThreads editor={editor} threads={threads} />
```

<Figure>
  <Image
    src="/assets/text-editor/anchored-threads.jpg"
    alt="AnchoredThreads"
    width={960}
    height={558}
  />
</Figure>

Takes a list of threads retrieved from [`useThreads`][] and renders them to the
page. Each thread is displayed at the same vertical coordinates as its
corresponding text highlight. If multiple highlights are in the same location,
each thread is placed in order below the previous thread.

```tsx
// +++
import { useThreads } from "@liveblocks/react/suspense";
// +++
import {
  useLiveblocksExtension,
  FloatingComposer,
  // +++
  AnchoredThreads,
  // +++
} from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension();

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  // +++
  const { threads } = useThreads();
  // +++

  return (
    <div>
      <EditorContent editor={editor} />
      <FloatingComposer editor={editor} style={{ width: "350px" }} />
      // +++
      <AnchoredThreads
        editor={editor}
        threads={threads}
        style={{ width: "350px" }}
      />
      // +++
    </div>
  );
}
```

<Banner title="Resolved threads">

The `AnchoredThreads` component automatically excludes resolved threads from
display. Any resolved threads passed in the threads list will not be shown.

</Banner>

#### Recommended usage [#AnchoredThreads-recommended-usage]

[`FloatingThreads`][] and [`AnchoredThreads`][] have been designed to work
together to provide the optimal experience on mobile and desktop. We generally
recommend using both components, hiding one on smaller screens, as we are below
with Tailwind classes. Most apps also don’t need to display resolved threads, so
we can filter those out with a [`useThreads`][] option.

```tsx
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-tiptap";
import { Editor } from "@tiptap/react";

function ThreadOverlay({ editor }: { editor: Editor | null }) {
  const { threads } = useThreads({ query: { resolved: false } });

  return (
    <>
      <FloatingThreads
        editor={editor}
        threads={threads}
        className="w-[350px] block md:hidden"
      />
      <AnchoredThreads
        editor={editor}
        threads={threads}
        className="w-[350px] hidden sm:block"
      />
    </>
  );
}
```

```tsx title="Alternatively use a media query hook" isCollapsed isCollapsable
import { useSyncExternalStore } from "react";
import { useThreads } from "@liveblocks/react/suspense";
import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-tiptap";
import { Editor } from "@tiptap/react";

function ThreadOverlay({ editor }: { editor: Editor | null }) {
  const { threads } = useThreads({ query: { resolved: false } });
  // +++
  const isMobile = useIsMobile();
  // +++

  // +++
  if (isMobile) {
    return (
      <FloatingThreads
        editor={editor}
        threads={threads}
        style={{ width: "350px" }}
      />
    );
  }
  // +++

  // +++
  return (
    <AnchoredThreads
      editor={editor}
      threads={threads}
      style={{ width: "350px" }}
    />
  );
  // +++
}

export function useIsMobile() {
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

function subscribe(callback: () => void) {
  const query = window.matchMedia("(max-width: 1024px)");

  query.addEventListener("change", callback);
  return () => query.removeEventListener("change", callback);
}

function getSnapshot() {
  const query = window.matchMedia("(max-width: 1024px)");
  return query.matches;
}
```

We can place this component inside [`ClientSideSuspense`][] to prevent it
rendering until threads have loaded.

```tsx
<div>
  <EditorContent editor={editor} />
  <FloatingComposer editor={editor} style={{ width: "350px" }} />
  // +++
  <ClientSideSuspense fallback={null}>
    <ThreadOverlay editor={editor} />
  </ClientSideSuspense>
  // +++
</div>
```

#### Customization [#AnchoredThreads-customization]

The `AnchoredThreads` component acts as a wrapper around each [`Thread`][]. It
has no width, so setting this is required, and each thread will take on the
width of the wrapper. You can treat the component like you would a `div`, using
classes, listeners, and more.

```tsx
<AnchoredThreads
  editor={editor}
  threads={threads}
  style={{ width: "350px" }}
  className="my-anchored-thread"
/>
```

To apply styling to each [`Thread`][], you can pass a custom `Thread` property
to `components` and modify this in any way.

```tsx
import { Thread } from "@liveblocks/react-ui";

<AnchoredThreads
  editor={editor}
  threads={threads}
  style={{ width: "350px" }}
  className="my-anchored-thread"
  // +++
  components={{
    Thread: (props) => (
      <Thread
        {...props}
        className="border shadow"
        style={{ background: "white" }}
      />
    ),
  }}
  // +++
/>;
```

You can return any custom `ReactNode` here, including anything from a simple
wrapper around `Thread`, up to a full custom `Thread` component built using our
[Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment).

```tsx
import { Comment } from "@liveblocks/react-ui/primitives";

<AnchoredThreads
  editor={editor}
  threads={threads}
  style={{ width: "350px" }}
  className="my-anchored-thread"
  components={{
    Thread: (props) => (
      // +++
      <div>
        {props.thread.comments.map((comment) => (
          <Comment.Body
            key={comment.id}
            body={comment.body}
            components={/* ... */}
          />
        ))}
      </div>
      // +++
    ),
  }}
/>;
```

##### Modifying thread floating positions

Using CSS variables you can modify the gap between threads, and the horizontal
offset that’s added when a thread is selected.

```css
.lb-tiptap-anchored-threads {
  /* Minimum gap between threads */
  --lb-tiptap-anchored-threads-gap: 8px;

  /* How far the active thread is offset to the left */
  --lb-tiptap-anchored-threads-active-thread-offset: 12px;
}
```

#### Props [#AnchoredThreads-props]

<PropertiesList>
  <PropertiesListItem name="threads" type="ThreadData[]" required>
    The threads to display.
  </PropertiesListItem>
  <PropertiesListItem
    name="components"
    type="Partial<AnchoredThreadsComponents>"
  >
    Override the component’s components.
  </PropertiesListItem>
  <PropertiesListItem
    name="components.Thread"
    type="(props: ThreadProps) => ReactNode"
  >
    Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread)
    component.
  </PropertiesListItem>
</PropertiesList>

### HistoryVersionPreview [@badge=beta]

The `HistoryVersionPreview` component allows you to display a preview of a
specific version of your Tiptap editor’s content. It also contains a button and
logic for restoring. To render a list of versions, see
[`VersionHistory`](/docs/api-reference/liveblocks-react-ui#Version-History).

#### Usage [#HistoryVersionPreview-usage]

```tsx
import { HistoryVersionPreview } from "@liveblocks/react-tiptap";

function VersionPreview({ selectedVersion, onVersionRestore }) {
  return (
    <HistoryVersionPreview
      version={selectedVersion}
      onVersionRestore={onVersionRestore}
    />
  );
}
```

#### Props [#HistoryVersionPreview-props]

<PropertiesList>
  <PropertiesListItem name="version" type="HistoryVersion" required>
    The version of the editor content to preview.
  </PropertiesListItem>
  <PropertiesListItem
    name="onVersionRestore"
    type="(version: HistoryVersion) => void"
  >
    Callback function called when the user chooses to restore this version.
  </PropertiesListItem>
</PropertiesList>

The `HistoryVersionPreview` component renders a read-only view of the specified
version of the editor content. It also provides a button for users to restore
the displayed version.

### AiToolbar [@badge=beta]

<Banner title="Private beta">

AI Copilots is currently in private beta. If you would like access to the beta,
please [contact us](https://liveblocks.io/contact/sales). We’d love to hear from
you.

</Banner>

Displays a floating AI toolbar near the current Tiptap selection, allowing you
to use AI to apply changes to the document or ask questions about it.

```tsx
<AiToolbar editor={editor} />
```

Pass your Tiptap `editor` to use the component, and enable (or customize) the AI
option in [`useLiveblocksExtension`][].

```tsx
import {
  useLiveblocksExtension,
  // +++
  AiToolbar,
  // +++
  FloatingToolbar,
} from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension({
    // +++
    ai: true,
    // +++
  });

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  return (
    <div>
      <EditorContent editor={editor} />
      // +++
      <AiToolbar editor={editor} />
      // +++
      <FloatingToolbar editor={editor} />
    </div>
  );
}
```

#### Opening the toolbar [#AiToolbar-opening]

To open the `AiToolbar`, you need to call the `askAi`
[command](https://tiptap.dev/docs/editor/api/commands) added by Liveblocks.

```tsx
import { Editor } from "@tiptap/react";

function Toolbar({ editor }: { editor: Editor | null }) {
  if (!editor) {
    return null;
  }

  return (
    <button
      onClick={() => {
        // +++
        editor.chain().focus().askAi().run();
        // +++
      }}
    >
      ✨ Ask AI
    </button>
  );
}
```

You can also pass a prompt to the `askAi` command and it will directly request
it when opening the toolbar.

```tsx
<button
  onClick={() => {
    // +++
    editor.chain().focus().askAi("Add emojis to the text").run();
    // +++
  }}
>
  😃 Emojify
</button>
```

#### Customizing suggestions [#AiToolbar-suggestions]

By default, the AI toolbar displays a list of suggestions (e.g. “Fix mistakes”,
“Explain”, etc). These can be customized via the `suggestions` prop and the
[`AiToolbar.Suggestion`][] component.

```tsx
<AiToolbar
  editor={editor}
  // +++
  suggestions={
    <>
      <AiToolbar.SuggestionsLabel>Suggested</AiToolbar.SuggestionsLabel>
      <AiToolbar.Suggestion>Fix mistakes</AiToolbar.Suggestion>
      <AiToolbar.Suggestion prompt="Add emojis to the text">
        Emojify
      </AiToolbar.Suggestion>
      <AiToolbar.SuggestionsSeparator />
      <AiToolbar.Suggestion icon={<Icon.Sparkles />}>
        Continue writing
      </AiToolbar.Suggestion>
    </>
  }
  // +++
/>
```

Doing so will override the default suggestions, instead you can use a function
to keep them while adding your own.

```tsx
<AiToolbar
  editor={editor}
  // +++
  suggestions={({ children }) => (
    <>
      {children}
      // +++
      <AiToolbar.SuggestionsSeparator />
      <AiToolbar.SuggestionsLabel>Custom</AiToolbar.SuggestionsLabel>
      <AiToolbar.Suggestion>Custom suggestion</AiToolbar.Suggestion>
    </>
  )}
/>
```

#### Props [#AiToolbar-props]

<PropertiesList>
  <PropertiesListItem name="editor" type="Editor | null" required>
    The Tiptap editor.
  </PropertiesListItem>
  <PropertiesListItem name="offset" type="number">
    The vertical offset of the AI toolbar from the selection.
  </PropertiesListItem>
  <PropertiesListItem name="suggestions" type="ReactNode">
    The prompt suggestions to display below the AI toolbar.
  </PropertiesListItem>
</PropertiesList>

#### AiToolbar.Suggestion

A prompt suggestion displayed below the AI toolbar.

```tsx
import { AiToolbar } from "@liveblocks/react-tiptap";

<AiToolbar
  editor={editor}
  suggestions={
    <>
      // +++
      <AiToolbar.Suggestion>Fix mistakes</AiToolbar.Suggestion>
      // +++
    </>
  }
/>;
```

By default, selecting a suggestion will use its label from the `children` as the
prompt, this can be overridden with the `prompt` prop. Also optionally takes an
icon.

```tsx
import { AiToolbar } from "@liveblocks/react-tiptap";
import { Icon } from "@liveblocks/react-ui";

// "Translate to French" is displayed in the suggestion and used as the prompt
<AiToolbar.Suggestion>Translate to French</AiToolbar.Suggestion>

// "Emojify" is displayed in the suggestion but "Add emojis to the text" is used as the prompt
<AiToolbar.Suggestion prompt="Add emojis to the text">Emojify</AiToolbar.Suggestion>

// Custom icon
<AiToolbar.Suggestion icon={<div>?</div>}>Explain</AiToolbar.Suggestion>

// Using a Liveblocks icon
<AiToolbar.Suggestion icon={<Icon.QuestionMark />}>Explain</AiToolbar.Suggestion>
```

##### Props [#AiToolbarSuggestion-props]

<PropertiesList>
  <PropertiesListItem name="children" type="ReactNode">
    The suggestion’s label, used as the prompt if the `prompt` prop is not set.
  </PropertiesListItem>
  <PropertiesListItem name="icon" type="ReactNode">
    An optional icon displayed before the label.
  </PropertiesListItem>
  <PropertiesListItem name="prompt" type="string">
    The prompt to use instead of the label.
  </PropertiesListItem>
</PropertiesList>

#### AiToolbar.SuggestionsLabel

A label to describe a group of prompt suggestions displayed in the AI toolbar.

```tsx
import { AiToolbar } from "@liveblocks/react-tiptap";

<AiToolbar
  editor={editor}
  suggestions={
    <>
      // +++
      <AiToolbar.SuggestionsLabel>Translation</AiToolbar.SuggestionsLabel>
      // +++
      <AiToolbar.Suggestion>Translate in French</AiToolbar.Suggestion>
      <AiToolbar.Suggestion>Translate in English</AiToolbar.Suggestion>
    </>
  }
/>;
```

#### AiToolbar.SuggestionsSeparator

A separator between groups of prompt suggestions displayed in the AI toolbar.

```tsx
import { AiToolbar } from "@liveblocks/react-tiptap";

<AiToolbar
  editor={editor}
  suggestions={
    <>
      <AiToolbar.Suggestion>Translate in French</AiToolbar.Suggestion>
      <AiToolbar.Suggestion>Translate in English</AiToolbar.Suggestion>
      // +++
      <AiToolbar.SuggestionsSeparator />
      // +++
      <AiToolbar.Suggestion>Custom suggestion</AiToolbar.Suggestion>
    </>
  }
/>;
```

## Hooks

### useLiveblocksExtension

Liveblocks plugin for Tiptap that adds collaboration to your editor.
`liveblocks` should be passed to Tiptap’s `useEditor` as an extension.

```tsx
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  // +++
  const liveblocks = useLiveblocksExtension();
  // +++

  const editor = useEditor({
    extensions: [
      // +++
      liveblocks,
      // +++
      // ...
    ],
  });

  return (
    <div>
      <EditorContent editor={editor} />
    </div>
  );
}
```

A number of options can be applied.

```tsx
const liveblocks = useLiveblocksExtension({
  // +++
  initialContent: "Hello world",
  field: "editor-one",
  // +++

  // Other options
  // ...
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="liveblocks" type="Extension">
    Returns a Liveblocks [Tiptap
    extension](https://tiptap.dev/docs/editor/core-concepts/extensions#create-a-new-extension).
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="options.initialContent" type="Content">
    The initial content for the editor, if it’s never been set. [Learn
    more](#Setting-initial-content).
  </PropertiesListItem>
  <PropertiesListItem name="options.field" type="string">
    The name of this text editor’s field. Allows you to use multiple editors on
    one page, if each has a separate field value. [Learn
    more](#Multiple-editors).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.offlineSupport_experimental"
    defaultValue="false"
    type="boolean"
  >
    Experimental. Enable offline support using IndexedDB. This means that after
    the first load, documents will be stored locally and load instantly. [Learn
    more](#Offline-support).
  </PropertiesListItem>
  <PropertiesListItem
    name="options.comments"
    defaultValue="true"
    type="boolean"
  >
    Enable comments in the editor.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.mentions"
    defaultValue="true"
    type="boolean"
  >
    Enable mentions in the editor.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.ai"
    defaultValue="false"
    type="boolean | AiConfiguration"
  >
    Enable AI in the editor and optionally customize configuration options.
  </PropertiesListItem>
</PropertiesList>

#### Setting initial content

Initial content for the editor can be set with `initialContent`. This content
will only be used if the current editor has never been edited by any users, and
is ignored otherwise.

```tsx
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";

function TextEditor() {
  const liveblocks = useLiveblocksExtension({
    // +++
    initialContent: "<p>Hello world</p>",
    // +++
  });

  // ...
}
```

#### Multiple editors

It’s possible to use multiple editors on one page by passing values to the
`field` property. Think of it like an ID for the current editor.

```tsx
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";

function TextEditor() {
  const liveblocks = useLiveblocksExtension({
    // +++
    field: "editor-one",
    // +++
  });

  // ...
}
```

Here’s an example of how multiple editors may be set up.

```tsx
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditors() {
  return (
    <div>
      // +++
      <TextEditor field="one" />
      <TextEditor field="two" />
      // +++
    </div>
  );
}

function TextEditor({ field }: { field: string }) {
  // +++
  const liveblocks = useLiveblocksExtension({ field });
  // +++

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  return (
    <div>
      <EditorContent editor={editor} />
    </div>
  );
}
```

#### Offline support [@badge=experimental]

It’s possible to enable offline support in your editor with an experimental
option. This means that once a document has been opened, it’s saved locally on
the browser, and can be shown instantly without a loading screen. As soon as
Liveblocks connects, any remote changes will be synchronized, without any load
spinner. Enable this by passing a `offlineSupport_experimental` value.

```tsx
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";

function TextEditor() {
  const liveblocks = useLiveblocksExtension({
    offlineSupport_experimental: true,
  });

  // ...
}
```

To make sure that your editor loads instantly, you must structure your app
carefully to avoid any Liveblocks hooks and [`ClientSideSuspense`][] components
from triggering a loading screen. For example, if you’re displaying threads in
your editor with [`useThreads`][], you must place this inside a separate
component and wrap it in [`ClientSideSuspense`][].

```tsx
"use client";

import { ClientSideSuspense, useThreads } from "@liveblocks/react/suspense";
import {
  useLiveblocksExtension,
  AnchoredThreads,
  FloatingComposer,
} from "@liveblocks/react-tiptap";
import { Editor, EditorContent, useEditor } from "@tiptap/react";

export function TiptapEditor() {
  const liveblocks = useLiveblocksExtension({
    offlineSupport_experimental: true,
  });

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
    immediatelyRender: false,
  });

  return (
    <>
      <EditorContent editor={editor} />
      <FloatingComposer editor={editor} style={{ width: 350 }} />
      // +++
      <ClientSideSuspense fallback={null}>
        <Threads editor={editor} />
      </ClientSideSuspense>
      // +++
    </>
  );
}

// +++
function Threads({ editor }: { editor: Editor }) {
  const { threads } = useThreads();

  return <AnchoredThreads editor={editor} threads={threads} />;
}
// +++
```

#### Customizing AI components [@badge=beta]

<Banner title="Private beta">

AI Copilots is currently in private beta. If you would like access to the beta,
please [contact us](https://liveblocks.io/contact/sales). We’d love to hear from
you.

</Banner>

By default, AI components like [`AiToolbar`][] use the term `"AI"`. This can be
customized with the `ai.name` option in [`useLiveblocksExtension`][]. This value
will be used throughout the AI components: `"Ask {name}"` in the [`Toolbar`][]/
[`FloatingToolbar`][] default buttons, `"{name} is thinking…"` in the
[`AiToolbar`][], etc.

```tsx
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
import { useEditor } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension({
    ai: {
      // +++
      // "Ask Liveblocks anything…", "Liveblocks is thinking…", etc
      name: "Liveblocks",
      // +++
    },
  });

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  // ...
}
```

If you’re after visual customization, AI components like [`AiToolbar`][]
integrate with the rest of Liveblocks' styles, heavily using tokens like
`--lb-accent` for example. Learn more about [styling](#Stylesheets).

#### Generating AI toolbar responses [@badge=beta]

<Banner title="Private beta">

AI Copilots is currently in private beta. If you would like access to the beta,
please [contact us](https://liveblocks.io/contact/sales). We’d love to hear from
you.

</Banner>

By default, the [`AiToolbar`][] component sends its requests to Liveblocks to
generate its responses. This can be customized via the
`ai.resolveContextualPrompt` option in [`useLiveblocksExtension`][]. This option
accepts an async function which will be called by the AI toolbar whenever a
prompt is requested, it will receive the prompt and some context (the document’s
and selection’s text, the previous request if it’s a follow-up, etc) and is
expected to return the type of response and the text to use.

```tsx
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
import { useEditor } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension({
    ai: {
      // +++
      resolveContextualPrompt: async ({ prompt, context, signal }) => {
        const response = await fetch("/api/contextual-prompt", {
          method: "POST",
          body: JSON.stringify({ prompt, context }),
          signal,
        });

        return response.json();
      },
      // +++
    },
  });

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  // ...
}
```

### useIsEditorReady

Used to check if the editor content has been loaded or not, helpful for
displaying a loading skeleton.

```ts
import { useIsEditorReady } from "@liveblocks/react-tiptap";

const status = useIsEditorReady();
```

Here’s how it can be used in the context of your editor.

```tsx
// +++
import {
  useIsEditorReady,
  useLiveblocksExtension,
} from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";
// +++

function TextEditor() {
  const liveblocks = useLiveblocksExtension();
  // +++
  const ready = useIsEditorReady();
  // +++

  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });

  return (
    <div>
      // +++
      {!ready ? <div>Loading...</div> : <EditorContent editor={editor} />}
      // +++
    </div>
  );
}
```

## Stylesheets

React Tiptap comes with default styles, and these can be imported into the root
of your app or directly into a CSS file with `@import`. Note that you must also
install and import a stylesheet from
[`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui) to use these
styles.

```tsx
import "@liveblocks/react-ui/styles.css";
import "@liveblocks/react-tiptap/styles.css";
```

### Customizing your styles

Adding dark mode and customizing your styles is part of `@liveblocks/react-ui`,
learn how to do this under
[styling and customization](/docs/api-reference/liveblocks-react-ui#Styling-and-customization).

[`useLiveblocksExtension`]: #useLiveblocksExtension
[`Thread`]: /docs/api-reference/liveblocks-react-ui#Thread
[`Composer`]: /docs/api-reference/liveblocks-react-ui#Composer
[`useThreads`]: /docs/api-reference/liveblocks-react#useThreads
[`Icon`]: /docs/api-reference/liveblocks-react-ui#Icon
[`Toolbar`]: #Toolbar
[`Toolbar.Button`]: #Toolbar.Button
[`Toolbar.Toggle`]: #Toolbar.Toggle
[`Toolbar.BlockSelector`]: #Toolbar.BlockSelector
[`Toolbar.Separator`]: #Toolbar.Separator
[`Toolbar.SectionHistory`]: #Toolbar.SectionHistory
[`Toolbar.SectionInline`]: #Toolbar.SectionInline
[`Toolbar.SectionCollaboration`]: #Toolbar.SectionCollaboration
[`FloatingToolbar`]: #FloatingToolbar
[`FloatingComposer`]: #FloatingComposer
[`FloatingThreads`]: #FloatingThreads
[`AnchoredThreads`]: #AnchoredThreads
[`AiToolbar`]: #AiToolbar
[`AiToolbar.Suggestion`]: #AiToolbar.Suggestion
[`AiToolbar.SuggestionsLabel`]: #AiToolbar.SuggestionsLabel
[`AiToolbar.SuggestionsSeparator`]: #AiToolbar.SuggestionsSeparator
[`ClientSideSuspense`]: /docs/api-reference/liveblocks-react#ClientSideSuspense

---
meta:
  title: "@liveblocks/react-ui"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/react-ui package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/react-ui` provides you with [React](https://react.dev/) components
to build collaborative experiences. Read our
[Comments](/docs/get-started/comments) and
[Notifications](/docs/get-started/notifications) get started guides to learn
more.

## AI Copilots

### Default components [#AI-Copilots-Components]

#### AiChat

Displays an interactive AI chat. AI can
[use knowledge](/docs/api-reference/liveblocks-react#RegisterAiKnowledge) and
[run actions or display content via tools](/docs/api-reference/liveblocks-react#RegisterAiTool).

```tsx
<AiChat chatId="my-chat-id" />
```

<Figure>
  <Image
    src="/assets/ai-copilots/ai-chat.jpg"
    alt="AI Chat"
    width={768}
    height={446}
    style={{ borderRadius: 12 }}
  />
</Figure>

Each chat is stored permanently, and is identified by its unique `chatId`. Chats
are only visible to the [authenticated user](/docs/authentication) who created
the chat.

```tsx
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return <AiChat chatId="my-chat-id" />;
}
```

##### Assigning a copilot [#AiChat-copilot]

Use a custom copilot in your chat. You can define copilots with custom prompts &
settings in the [Liveblocks dashboard](/dashboard), passing your API key from
OpenAI, Anthropic, or Google. Copy the copilot's ID and pass it to the
`copilotId` prop.

```tsx
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <AiChat
      chatId="my-chat-id"
      // +++
      copilotId="co_a7Gd3x..."
      // +++
    />
  );
}
```

Dynamically switching copilots is possible, and messages will use whichever
`copilotId` is set when a message is sent.

##### Show placeholder content in new chats [#AiChat-placeholder]

In chats without messages, you can display placeholder content to welcome and
guide the user. To set this content, use the `Empty` property under
`components`.

```tsx
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <AiChat
      chatId="my-chat-id"
      // +++
      components={{ Empty: <div>I'm an empty chat!</div> }}
      // +++
    />
  );
}
```

Additionally, you can add suggestion buttons which will automatically submit new
messages to the chat when clicked. Create them with
[`useSendAiMessage`](/docs/api-reference/liveblocks-react#useSendAiMessage).

```tsx
import { useSendAiMessage } from "@liveblocks/react";
import { AiChat } from "@liveblocks/react-ui";

function Chat({ chatId }: { chatId: string }) {
  // +++
  const sendAiMessage = useSendAiMessage(chatId);
  // +++

  return (
    <AiChat
      chatId="my-chat-id"
      components={{
        Empty: (
          <div>
            <div>Suggestions</div>
            // +++
            <button onClick={() => sendAiMessage("What's new?")}>
              Update me
            </button>
            <button onClick={() => sendAiMessage("Create a new document")}>
              Draft a document
            </button>
            // +++
          </div>
        ),
      }}
    />
  );
}
```

##### List the user’s chats and switch between them [#AiChat-chat-listing]

You can display a list of all chats created by the current user with
[`useAiChats`](/docs/api-reference/liveblocks-react#useAiChats). For example,
you can render a list of buttons that allow you to switch between chats. In each
button, you can display the chat’s automatically generated title, as seen below.
Chats can be deleted with
[`useDeleteAiChat`](/docs/api-reference/liveblocks-react#useDeleteAiChat).

```tsx
import { useState } from "react";
import { AiChat } from "@liveblocks/react-ui";
import { useAiChats } from "@liveblocks/react";
import { Timestamp } from "@liveblocks/react-ui/primitives";

function Chats() {
  // +++
  const { chats, error, isLoading } = useAiChats();
  const [chatId, setChatId] = useState();
  const deleteChat = useDeleteAiChat();
  // +++

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div style={{ display: "flex" }}>
      <ul>
        // +++
        {chats.map((chat) => (
          <li key={chat.id}>
            <button onClick={() => setChatId(chat.id)}>
              {chat.title || "Untitled"}
            </button>
            <Timestamp date={chat.lastMessageAt || chat.createdAt} />
            <button onClick={() => deleteChat(chat.id)}>❌</button>
          </li>
        ))}
        // +++
      </ul>
      // +++
      <AiChat chatId={chatId} />
      // +++
    </div>
  );
}
```

##### Display the chat’s title [#AiChat-title]

Each chat has a `title` property, automatically generated by AI. The title of a
new chat starts empty, and is updated after AI receives the first message and
writes a response. You can render this alongside your chat with
[`useAiChat`](/docs/api-reference/liveblocks-react#useAiChat).

```tsx
import { AiChat } from "@liveblocks/react-ui";
import { useAiChat } from "@liveblocks/react";

function ChatWithTitle({ chatId }: { chatId: string }) {
  // +++
  const { chat, error, isLoading } = useAiChat(chatId);
  // +++

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      // +++
      <h1>{chat.title || "Untitled chat"}</h1>
      // +++
      <AiChat chatId={chatId} />
    </div>
  );
}
```

##### Add front-end knowledge [#AiChat-front-end-knowledge]

You can add front-end knowledge to chats, meaning the AI will understand the
information you pass, and will answer questions or call tools based on it. This
is particularly helpful for passing user info, app state, and small contextual
knowledge.

It’s generally recommended to use
[`RegisterAiKnowledge`](/docs/api-reference/liveblocks-react#RegisterAiKnowledge)
for adding knowledge, as this will add knowledge to all AI features on the page.
However, if you’d like knowledge that is specific to one chat, you can add it
with the `knowledge` prop on `AiChat`. No other chats will have access to this
knowledge.

```tsx
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <AiChat
      chatId="my-chat-id"
      // +++
      knowledge={[
        { description: "The current user's payment plan", value: "Enterprise" },
        {
          description: "The current user's info",
          value: {
            name: "Jody Hekla",
            email: "jody@liveblocks.io",
            teams: ["Engineering", "Product"],
          },
        },
      ]}
      // +++
    />
  );
}
```

##### Add back-end knowledge [#AiChat-back-end-knowledge]

You can add back-end knowledge to chats, meaning the AI will understand the
information you pass, and can answer questions or call tools based on it. This
is a way to pass large amounts of project-wide information, for example complex
documentation.

{/* TODO screenshot */}

When creating or editing a copilot in the [Liveblocks dashboard](/dashboard)
navigate to the Knowledge tab. Within here you can upload any relevant files, or
submit websites for indexing. Your copilot will internalize this knowledge using
retrieval-augmented generation (RAG).

##### Adjusting the chat’s width [#AiChat-width]

When using the default `inset` layout, it’s possible to adjust the chat’s width
by setting the `--lb-ai-chat-container-width` CSS variable. This allows the
chat’s scroll window to stay full width, whilst keeping the composer and
messages centered in the middle.

```css
.lb-ai-chat {
  --lb-ai-chat-container-width: 600px;
}
```

```tsx title="Set variable in React" isCollapsable isCollapsed
import { CSSProperties } from "react";
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <AiChat
      chatId="my-chat-id"
      // +++
      style={{ "--lb-ai-chat-container-width": 600 } as CSSProperties}
      // +++
    />
  );
}
```

```tsx title="Set variable in Tailwind CSS" isCollapsable isCollapsed
import { CSSProperties } from "react";
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <AiChat
      chatId="my-chat-id"
      // +++
      className="[--lb-ai-chat-container-width:600px]"
      // +++
    />
  );
}
```

##### Compact layout mode [#AiChat-compact-layout]

An alternate `compact` layout mode is available, ideal for smaller UI components
such as pop-up windows. Compact layout mode removes the shadow and padding on
the composer, makes it full-width, and displays a border above it.

```tsx
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <AiChat
      chatId="my-chat-id"
      // +++
      layout="compact"
      // +++
    />
  );
}
```

##### Change background color [#AiChat-background]

You can change the background color of the chat by setting the `--lb-background`
CSS variable on `.lb-ai-chat`.

```css
.lb-ai-chat {
  --lb-background: #eeeeee;
}
```

```tsx title="Set variable in React" isCollapsable isCollapsed
import { CSSProperties } from "react";
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <AiChat
      chatId="my-chat-id"
      // +++
      style={{ "--lb-background": "#EEEEEE" } as CSSProperties}
      // +++
    />
  );
}
```

```tsx title="Set variable in Tailwind CSS" isCollapsable isCollapsed
import { CSSProperties } from "react";
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <AiChat
      chatId="my-chat-id"
      // +++
      className="[--lb-background:#EEEEEE]"
      // +++
    />
  );
}
```

##### Customize CSS variables and classes [#AiChat-variables-and-classes]

You can customize the default styles of the chat by modifying CSS variables and
classes prefixed with `lb`. Here are some examples.

```css
/* Lowers spacing and shrinks font size */
.lb-ai-chat {
  --lb-spacing: 0.6em;
  font-size: 14px;
}

/* Removes composer shadow and adds border */
.lb-ai-chat-composer {
  box-shadow: 0;
  border: 1px solid #f0f0f0;
}

/* Removes padding below the composer */
.lb-ai-chat-footer {
  padding-bottom: 0;
}
```

##### Customize how Markdown is rendered [#AiChat-markdown]

You can customize how Markdown is rendered in messages by passing components to
the `components` prop. A full list is [available here](#AiChat-components).

```tsx
<AiChat
  chatId="my-chat-id"
  components={{
    // +++
    markdown: {
      // Example: Use custom paragraph styles
      Paragraph: ({ children }) => <p className="my-3">{children}</p>,

      // Example: Use an existing component for quotes
      Blockquote: ({ children }) => <MyQuote>{children}</MyQuote>,

      // Example: Use `next/link` instead of default `<a>` tag
      Link: ({ children, href }) => <Link href={href || ""}>{children}</Link>,

      // Example: Use an external library to add syntax highlighting to code blocks
      CodeBlock: ({ language, code }) => (
        <SyntaxHighlighter language={language}>{code}</SyntaxHighlighter>
      ),

      // `Heading`, `Inline`, `List`, `Table`, `Image`, `Separator`, etc.
      // ...
    },
    // +++
  }}
/>
```

##### Props [#AiChat-props]

<PropertiesList>
  <PropertiesListItem name="chatId" type="string" required>
    The unique identifier for the chat. Each chat is stored permanently and is
    only visible to the authenticated user who created it.
  </PropertiesListItem>
  <PropertiesListItem name="autoFocus" type="boolean">
    Whether to automatically focus the composer input when the chat loads.
    Defaults to `false`.
  </PropertiesListItem>
  <PropertiesListItem name="copilotId" type="string">
    The ID of the custom copilot to use for this chat. Copy this from your
    copilot configuration in the [Liveblocks
    dashboard](https://liveblocks.io/dashboard/copilots).
  </PropertiesListItem>
  <PropertiesListItem name="knowledge" type="AiKnowledgeSource[]">
    Array of knowledge sources specific to this chat. This knowledge will only
    be available to this chat instance and not to other AI features on the page.
  </PropertiesListItem>
  <PropertiesListItem name="tools" type="Record<string, AiToolDefinition>">
    Object mapping tool names to tool definitions that should be available in
    this chat.
  </PropertiesListItem>
  <PropertiesListItem
    name="onComposerSubmit"
    type="function"
    detailedType="(comment: AiComposerSubmitMessage, event: FormEvent<HTMLFormElement>) => Promise<void> | void"
  >
    The event handler called when the composer is submitted.
  </PropertiesListItem>
  <PropertiesListItem name="layout" type="'inset' | 'compact'">
    The layout mode for the chat. Use `'inset'` (default) for standalone chats,
    or `'compact'` for embedded scenarios like pop-up windows.
  </PropertiesListItem>
  <PropertiesListItem name="overrides" type="AiChatOverrides">
    Advanced customization options for overriding default chat behavior and
    styling.
  </PropertiesListItem>
  <PropertiesListItem name="components" type="AiChatComponents">
    Custom components to override specific parts of the chat UI, such as the
    `Empty` placeholder component or Markdown components.
  </PropertiesListItem>
  <PropertiesListItem name="responseTimeout" type="number">
    The time, in milliseconds, before an AI response will timeout. Defaults to
    30_000.
  </PropertiesListItem>
  <PropertiesListItem name="showReasoning" type="boolean | 'during'">
    Whether to show reasoning. Defaults to `true`. If set to `'during'`,
    reasoning will only be shown during reasoning.
  </PropertiesListItem>
  <PropertiesListItem
    name="showRetrievals"
    type="boolean | 'during' | { ... } | undefined"
    advancedType="boolean | 'during' | { web?: boolean | 'during', knowledge?: boolean | 'during' } | undefined"
  >
    Whether to show retrievals. Defaults to `true`. If set to `'during'`,
    retrievals will only be shown during retrieval.
  </PropertiesListItem>
  <PropertiesListItem name="showSources" type="boolean">
    Whether to show sources. Defaults to `true`.
  </PropertiesListItem>
  <PropertiesListItem name="className" type="string">
    CSS class name to apply to the chat container.
  </PropertiesListItem>
  <PropertiesListItem name="style" type="CSSProperties">
    Inline styles to apply to the chat container. Useful for setting CSS custom
    properties.
  </PropertiesListItem>
</PropertiesList>

###### components [#AiChat-components]

Override specific parts of `AiChat` with custom components.

<PropertiesList>
  <PropertiesListItem
    name="Empty"
    type="({ chatId: string, copilotId?: string }) => ReactNode"
  >
    The component used to render the empty state of the chat. Defaults to
    nothing.
  </PropertiesListItem>
  <PropertiesListItem name="Loading" type="() => ReactNode">
    The component used to render the loading state of the chat. Defaults to a
    loading spinner.
  </PropertiesListItem>
  <PropertiesListItem name="markdown" type="Partial<MarkdownComponents>">
    The components used to render Markdown content.
  </PropertiesListItem>
  <PropertiesListItem
    name="markdown.Paragraph"
    type="({ children: ReactNode }) => ReactNode"
  >
    The component used to render paragraphs.
  </PropertiesListItem>
  <PropertiesListItem
    name="markdown.Inline"
    detailedType={`({ type: "strong" | "em" | "code" | "del", children: ReactNode }) => ReactNode`}
  >
    The component used to render inline elements (bold, italic, strikethrough,
    and inline code).
  </PropertiesListItem>
  <PropertiesListItem
    name="markdown.Link"
    type={`({ href: string, title?: string, children: ReactNode }) => ReactNode`}
  >
    The component used to render links.
  </PropertiesListItem>
  <PropertiesListItem
    name="markdown.Heading"
    type={`({ level: 1 | 2 | 3 | 4 | 5 | 6, children: ReactNode }) => ReactNode`}
  >
    The component used to render headings.
  </PropertiesListItem>
  <PropertiesListItem
    name="markdown.Blockquote"
    type="({ children: ReactNode }) => ReactNode"
  >
    The component used to render blockquotes.
  </PropertiesListItem>
  <PropertiesListItem
    name="markdown.CodeBlock"
    type={`({ code: string, language?: string }) => ReactNode`}
  >
    The component used to render code blocks.
  </PropertiesListItem>
  <PropertiesListItem
    name="markdown.Image"
    type={`({ src: string, alt: string, title?: string }) => ReactNode`}
  >
    The component used to render images.
  </PropertiesListItem>
  <PropertiesListItem
    name="markdown.List"
    detailedType={`({ type: "ordered" | "unordered", items: { checked?: boolean, children: ReactNode }[], start?: number }) => ReactNode`}
  >
    The component used to render lists.
  </PropertiesListItem>
  <PropertiesListItem
    name="markdown.Table"
    detailedType={`({ headings: { align?: "left" | "center" | "right"; children: ReactNode }[], rows: { align?: "left" | "center" | "right"; children: ReactNode }[] }) => ReactNode`}
  >
    The component used to render tables.
  </PropertiesListItem>
  <PropertiesListItem name="markdown.Separator" type="() => ReactNode">
    The component used to render separators.
  </PropertiesListItem>
</PropertiesList>

#### AiTool

Displays [AI tool calls](/docs/ready-made-features/ai-copilots/features#tools)
and their progress. Can be customized for many different UIs.

{/* TODO AI tool image */}

```tsx
<AiTool />
```

By default, `AiTool` will display the name of the current tool, and a loading
spinner as it runs.

```tsx
import { defineAiTool } from "@liveblocks/client";
import { RegisterAiTool } from "@liveblocks/react";
import { AiTool, AiChat } from "@liveblocks/react-ui";

function App() {
  return (
    <>
      <RegisterAiTool
        name="get-weather"
        tool={defineAiTool()({
          description: "Get current weather information",
          parameters: {
            type: "object",
            properties: {
              location: { type: "string", description: "City name" },
            },
            required: ["location"],
            additionalProperties: false,
          },
          execute: async (args) => {
            const { temperature, condition } = await __getWeather__(
              args.location
            );
            return { data: { temperature, condition } };
          },
          render: ({ result }) => (
            // +++
            <AiTool>
              {result ? (
                <div>
                  {result.temperature}°F - {result.condition}
                </div>
              ) : null}
            </AiTool>
            // +++
          ),
        })}
      />
      <AiChat chatId="my-chat" />
    </>
  );
}
```

Optionally, you can provide a `title` and `icon` to render the UI differently.

```tsx
// Title
render: () => <AiTool title="Event booked" />

// Title and a collapsible description
render: () => <AiTool title="Event booked">We've booked the event!</AiTool>,

// Title and emoji icon
render: () => <AiTool title="Event booked" icon="📅" />,

// Title and icon component
render: () => <AiTool title="Event booked" icon={<Icon.Calendar />} />,

// Props are passed to the inner `div`
render: () => (
  <AiTool
    title="Event booked"
    style={{ marginLeft: 10 }}
    className="event-booked-tool"
    onMouseOver={() => console.log("Hovered")}
  />
),
```

##### Props [#AiTool-props]

<PropertiesList>
  <PropertiesListItem name="title" type="ReactNode">
    The title to display for the tool. If not provided, the tool name will be
    formatted as a human-readable string.
  </PropertiesListItem>
  <PropertiesListItem name="icon" type="ReactNode">
    Icon to display alongside the tool title. Can be an emoji string, React
    component, or any ReactNode.
  </PropertiesListItem>
  <PropertiesListItem name="children" type="ReactNode">
    Content to display inside the tool container. Typically used for
    tool-specific UI or descriptions.
  </PropertiesListItem>
  <PropertiesListItem name="variant" type="'block' | 'minimal'">
    The visual appearance of the tool. The `"block"` variant (default) displays
    the tool as a block with a border.
  </PropertiesListItem>
  <PropertiesListItem name="collapsed" type="boolean">
    Whether the tool content should be collapsed. When collapsed, only the title
    and icon are visible.
  </PropertiesListItem>
  <PropertiesListItem
    name="onCollapsedChange"
    type="(collapsed: boolean) => void"
  >
    Callback fired when the collapsed state changes. Use this to control the
    collapsed state externally.
  </PropertiesListItem>
  <PropertiesListItem name="collapsible" type="boolean">
    Whether the tool content can be collapsed. If set to `false`, clicking on it
    will have no effect. If there's no content, this prop has no effect.
  </PropertiesListItem>
  <PropertiesListItem name="className" type="string">
    CSS class name to apply to the tool container.
  </PropertiesListItem>
  <PropertiesListItem name="style" type="CSSProperties">
    Inline styles to apply to the tool container.
  </PropertiesListItem>
</PropertiesList>

All other HTML `div` props are also supported and will be passed through to the
underlying container element.

##### AiTool.Confirmation

Displays an AI tool with a confirmation UI. This allows you to create actions
that users must confirm or cancel before they’re run.

```tsx
<AiTool>
  <AiTool.Confirmation confirm={() => /* ... */} cancel={() => /* ... */} />
</AiTool>
```

{/* TODO ai tool confirmation image */}

Use the `confirm` and `cancel` props to define which actions should be taken
when the users clicks the buttons. You can return information that helps the AI
understand what has taken place, and data which you can use in `render` after
the tool is called.

```tsx
import { defineAiTool } from "@liveblocks/client";
import { RegisterAiTool } from "@liveblocks/react";
import { AiTool } from "@liveblocks/react-ui";

const deleteFileTool = defineAiTool<{ deletedFileName: string }>()({
  description: "Delete a file from the user's workspace",
  parameters: {
    type: "object",
    properties: {
      fileName: { type: "string", description: "Name of the file to delete" },
    },
    required: ["fileName"],
    additionalProperties: false,
  },
  render: ({ stage, args, result, types }) => {
    if (stage === "receiving") {
      return "Loading...";
    }
    return (
      <AiTool title="Delete File" icon="🗑️">
        {!result.data ? (
          // +++
          <AiTool.Confirmation
            // Make `confirm` and `cancel` type-safe
            types={types}
            confirm={async ({ fileName }) => {
              await deleteFile(fileName);
              return {
                data: { deletedFileName: fileName },
              };
            }}
          >
            Are you sure you want to delete {args.fileName}?
          </AiTool.Confirmation>
        ) : (
          // +++
          <div>Deleted {result.data.deletedFileName}</div>
        )}
      </AiTool>
    );
  },
});

function App() {
  return (
    <>
      <RegisterAiTool name="delete-file" tool={deleteFileTool} />
      <AiChat chatId="my-chat" />
    </>
  );
}
```

`AiTool.Confirmation` will display different content depending on the stage of
the tool. For example, the confirm and cancel buttons will disappear when
clicked.

###### Props [#AiTool.Confirmation-props]

<PropertiesList>
  <PropertiesListItem
    name="confirm"
    type="(args: TArgs) => Promise<ToolResultResponse>"
    required
  >
    Function called when the user clicks the confirm button. It can return data
    which will be stored and accessible in `render`, and optionally also a
    description for the AI to understand the result: `{ data: { formId: 123 }, description: "The user accepted and submitted the form" }`
  </PropertiesListItem>
  <PropertiesListItem
    name="cancel"
    type="(args: TArgs) => Promise<ToolResultResponse>"
  >
    Function called when the user clicks the cancel button.
  </PropertiesListItem>
  <PropertiesListItem name="children" type="ReactNode">
    Content to display in the confirmation UI. Typically a question or
    description of the action being confirmed.
  </PropertiesListItem>
  <PropertiesListItem name="variant" type="'destructive' | 'default'">
    The visual appearance of the confirmation UI.
  </PropertiesListItem>
  <PropertiesListItem
    name="overrides"
    type="Partial<GlobalOverrides & AiToolConfirmationOverrides>"
  >
    Override the component’s strings. It can be used the change the "confirm"
    and "cancel" labels.
  </PropertiesListItem>
</PropertiesList>

All other HTML `div` props are also supported and will be passed through to the
underlying element.

##### AiTool.Inspector

Displays formatted view of the JSON arguments sent to and results returned by
the AI during the current tool invocation. This is useful for debugging or for
providing developers with insight into the data exchanged within your app.

```tsx
<AiTool>
  <AiTool.Inspector />
</AiTool>
```

To use, simply include `<AiTool.Inspector />` inside an `<AiTool />` component
to display the tool’s input arguments and resulting output.

```tsx
import { defineAiTool } from "@liveblocks/client";
import { RegisterAiTool } from "@liveblocks/react";
import { AiTool, AiChat } from "@liveblocks/react-ui";

function App() {
  return (
    <>
      <RegisterAiTool
        name="toggle-todo"
        tool={defineAiTool()({
          description: "Toggle a todo's completion status",
          parameters: {
            type: "object",
            properties: {
              id: {
                description: "The id of the todo to toggle",
                type: "number",
              },
            },
            required: ["id"],
            additionalProperties: false,
          },
          execute: ({ id }) => {
            toggleTodo(id);
          },
          render: () => (
            <AiTool>
              // +++
              <AiTool.Inspector />
              // +++
            </AiTool>
          ),
        })}
      />
      <AiChat chatId="my-chat" />
    </>
  );
}
```

###### Props [#AiTool.Inspector-props]

All HTML `div` props are supported and will be passed through to the underlying
element.

## Comments

### Default components [#Components]

#### Thread

Displays a thread of comments. Each thread has a composer for creating replies.

```tsx
<Thread thread={thread} />
```

<Figure>
  <Image
    src="/assets/comments/thread.png"
    alt="Thread"
    width={768}
    height={446}
  />
</Figure>

Map through `threads` to render a list of the room’s threads and comments.
Threads can be retrieved with
[`useThreads`](/docs/api-reference/liveblocks-react#useThreads).

```tsx highlight="10"
import { Thread } from "@liveblocks/react-ui";
import { useThreads } from "@liveblocks/react/suspense";

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

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

##### Resolved and unresolved threads

A thread can be marked as resolved or unresolved via its `resolved` property.
The `Thread` component automatically handles this through its `resolved` toggle
button displayed by default.

You can additionally use `thread.resolved` to filter the displayed threads for
example. Or if you want to create your own `Thread` component using
[the primitives](/docs/ready-made-features/comments/primitives), you can use
[`useMarkThreadAsResolved`](/docs/api-reference/liveblocks-react#useMarkThreadAsResolved)
and
[`useMarkThreadAsUnresolved`](/docs/api-reference/liveblocks-react#useMarkThreadAsUnresolved)
to update the property.

##### Collapsed threads

You can collapse threads by setting the `maxVisibleComments` prop. If a thread
contains more comments than the limit set, some of the comments will be hidden
and a "Show more replies" button will be displayed instead. Clicking on it will
expand the thread to show all comments.

```tsx
<Thread thread={thread} maxVisibleComments={5} />
```

The first and last comments are always visible, and by default the oldest
comments are more likely to be hidden. You can customize this behavior by
setting `maxVisibleComments` to an object.

```tsx
// This is the default behavior, the same as `maxVisibleComments={5}`.
<Thread thread={thread} maxVisibleComments={{ max: 5, show: "newest" }} />

// Only show the last comment, and all the older ones to fit the limit.
<Thread thread={thread} maxVisibleComments={{ max: 5, show: "oldest" }} />

// Show as many old comments as new ones to fit the limit.
<Thread thread={thread} maxVisibleComments={{ max: 5, show: "both" }} />
```

##### Custom dropdown items

`Thread` shows a dropdown menu for threads and comments which contains actions
related to them: “Subscribe to thread”, “Edit comment”, “Delete comment”, etc.

The prop `commentDropdownItems` allows customizing the dropdown’s items, for
example adding new items. Items can be built with the `Comment.DropdownItem`
component which accepts an `onSelect` prop that is called when the item is
selected.

```tsx
<Thread
  commentDropdownItems={
    <>
      <Comment.DropdownItem
        onSelect={() => {
          console.log("Open details");
        }}
      >
        Details
      </Comment.DropdownItem>
      <Comment.DropdownItem
        onSelect={() => {
          console.log("Move comment");
        }}
      >
        Move
      </Comment.DropdownItem>
    </>
  }
/>
```

These new items will be displayed below the default items, but it’s possible to
change that by passing a function. This function receives a `children` prop
which contains the default items, so you can decide to display them above or
below your new items, or even not display them at all. This function also
receives a `comment` prop which contains the comment it’s attached to.

```tsx
<Thread
  // +++
  commentDropdownItems={({ children }) => {
    // +++
    return (
      <>
        <Comment.DropdownItem
          onSelect={() => {
            // +++
            openDetails(comment.id);
            // +++
          }}
        >
          Details
        </Comment.DropdownItem>
        // +++
        {/* The "Details" item will be displayed above the default items */}
        {children}
        // +++
      </>
    );
  }}
/>
```

The `Comment.DropdownItem` component also accepts an `icon` prop to display an
icon next to the item’s label.

```tsx
<Comment.DropdownItem
  onSelect={() => {
    openDetails(comment.id);
  }}
  // +++
  icon={<Icon.Details />}
  // +++
>
  Details
</Comment.DropdownItem>
```

[`Comment`](/docs/api-reference/liveblocks-react-ui#Comment) offers the same as
`Thread`’s `commentDropdownItems` but named `dropdownItems` instead.

##### Props [#Thread-props]

<PropertiesList>
  <PropertiesListItem name="thread" type="ThreadData" required>
    The thread to display.
  </PropertiesListItem>
  <PropertiesListItem
    name="showComposer"
    type='boolean | "collapsed"'
    defaultValue='"collapsed"'
  >
    How to show or hide the composer to reply to the thread.
  </PropertiesListItem>
  <PropertiesListItem
    name="showActions"
    type='boolean | "hover"'
    defaultValue='"hover"'
  >
    How to show or hide the actions.
  </PropertiesListItem>
  <PropertiesListItem name="showReactions" type="boolean" defaultValue="true">
    Whether to show reactions.
  </PropertiesListItem>
  <PropertiesListItem name="showAttachments" type="boolean" defaultValue="true">
    Whether to show attachments.
  </PropertiesListItem>
  <PropertiesListItem
    name="showComposerFormattingControls"
    type="boolean"
    defaultValue="true"
  >
    Whether to show the composer’s formatting controls.
  </PropertiesListItem>
  <PropertiesListItem
    name="blurComposerOnSubmit"
    type="boolean"
    defaultValue="true"
  >
    Whether to blur the composer editor when the composer is submitted.
  </PropertiesListItem>
  <PropertiesListItem
    name="showResolveAction"
    type="boolean"
    defaultValue="true"
  >
    Whether to show the action to resolve the thread.
  </PropertiesListItem>
  <PropertiesListItem
    name="maxVisibleComments"
    type="number | object"
    detailedType='number | { max: number; show: "oldest" | "newest" | "both" }'
    defaultValue="No limit"
  >
    The maximum number of comments to show.
  </PropertiesListItem>
  <PropertiesListItem
    name="indentCommentContent"
    type="boolean"
    defaultValue="true"
  >
    Whether to indent the comments’ content.
  </PropertiesListItem>
  <PropertiesListItem
    name="showDeletedComments"
    type="boolean"
    defaultValue="false"
  >
    Whether to show deleted comments.
  </PropertiesListItem>
  <PropertiesListItem
    name="commentDropdownItems"
    type="ReactNode | (props) => ReactNode"
    detailedType="ReactNode | ((props: { children: ReactNode, comment: CommentData }) => ReactNode)"
  >
    Add (or change) items to display in the comment’s dropdown.
  </PropertiesListItem>
  <PropertiesListItem
    name="onComposerSubmit"
    type="function"
    detailedType="(comment: ComposerSubmitComment, event: FormEvent<HTMLFormElement>) => Promise<void> | void"
  >
    The event handler called when the composer is submitted.
  </PropertiesListItem>
  <PropertiesListItem
    name="onResolvedChange"
    type="function"
    detailedType="(resolved: boolean) => void"
  >
    The event handler called when changing the resolved status.
  </PropertiesListItem>
  <PropertiesListItem
    name="onThreadDelete"
    type="function"
    detailedType="(thread: ThreadData) => void"
  >
    The event handler called when the thread is deleted. A thread is deleted
    when all its comments are deleted.
  </PropertiesListItem>
  <PropertiesListItem
    name="onCommentEdit"
    type="function"
    detailedType="(comment: CommentData) => void"
  >
    The event handler called when a comment is edited.
  </PropertiesListItem>
  <PropertiesListItem
    name="onCommentDelete"
    type="function"
    detailedType="(comment: CommentData) => void"
  >
    The event handler called when a comment is deleted.
  </PropertiesListItem>
  <PropertiesListItem
    name="onAuthorClick"
    type="function"
    detailedType="(userId: string, event: MouseEvent<HTMLElement>) => void"
  >
    The event handler called when clicking on a comment’s author.
  </PropertiesListItem>
  <PropertiesListItem
    name="onMentionClick"
    type="function"
    detailedType="(mention: MentionData, event: MouseEvent<HTMLElement>) => void"
  >
    The event handler called when clicking on a mention.
  </PropertiesListItem>
  <PropertiesListItem
    name="onAttachmentClick"
    type="function"
    detailedType="(args: CommentAttachmentArgs, event: MouseEvent<HTMLElement>) => void"
  >
    The event handler called when clicking on a comment’s attachment.
  </PropertiesListItem>
  <PropertiesListItem
    name="overrides"
    type="Partial<GlobalOverrides & ThreadOverrides & CommentOverrides & ComposerOverrides>"
  >
    Override the component’s strings.
  </PropertiesListItem>
</PropertiesList>

{/* TODO: Document classes and data attributes */}

#### Composer

Displays a composer for creating threads or comments.

```tsx
<Composer />
```

<Figure>
  <Image
    src="/assets/comments/composer.png"
    alt="Composer"
    width={768}
    height={446}
  />
</Figure>

By default, submitting the composer will create a new thread.

```tsx
import { Composer } from "@liveblocks/react-ui";

// Creates a new thread
function Component() {
  return <Composer />;
}
```

##### Adding thread metadata

If you’d like to attach custom metadata to the newly created thread, you can add
a `metadata` prop.

```tsx
import { Composer } from "@liveblocks/react-ui";

// Creates a new thread with custom metadata
function Component() {
  return (
    <Composer
      metadata={{
        // Custom metadata here, e.g. colors, coordinates
        color: "purple",
        x: 80,
        y: 120,
      }}
    />
  );
}
```

###### Typed metadata

You can use TypeScript to type your custom metadata by editing your config file.
Metadata properties can be `string`, `number`, or `boolean`.

```ts file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    // Set your custom metadata types
    // +++
    ThreadMetadata: {
      // Example types, e.g. colors, coordinates
      color: string;
      x: number;
      y: number;
    };
    CommentMetadata: {
      // Example types, e.g. tags, context, external IDs
      tag?: string;
      spam: boolean;
      slackMessageTs: string;
    };
    // +++

    // Other types
    // ...
  }
}
```

##### Replying to a thread

If you provide a `threadId`, then submitting the composer will add a new reply
to the thread.

```tsx
import { Composer } from "@liveblocks/react-ui";

// Adds a new comment to a thread
function Component({ threadId }) {
  return <Composer threadId={threadId} />;
}
```

##### Adding comment metadata

If you’d like to attach custom metadata to a reply, you can add a
`commentMetadata` prop.

```tsx
import { Composer } from "@liveblocks/react-ui";

// Creates a new reply to an existing thread with custom metadata
function Component({ threadId }) {
  return (
    <Composer
      threadId={threadId}
      commentMetadata={{
        // Custom metadata here, e.g. tags, context, external IDs
        tag: "important",
        spam: false,
      }}
    />
  );
}
```

##### Modifying a comment

If you provide both a `threadId` and a `commentId`, then submitting the composer
will edit the comment.

```tsx
import { Composer } from "@liveblocks/react-ui";

// Edits an existing comment
function Component({ threadId, commentId }) {
  return <Composer threadId={threadId} commentId={commentId} />;
}
```

##### Custom behavior

If you’d like to customize submission behavior, you can use
`event.preventDefault()` in `onComposerSubmit` to disable the default behavior
and call comment and thread mutation methods manually.

```tsx
import { Composer } from "@liveblocks/react-ui";
import { useEditComment, useAddReaction } from "@liveblocks/react/suspense";

// Custom submission behavior (edits a comment and adds a reaction)
function Component({ threadId, commentId }) {
  const editComment = useEditComment();
  const addReaction = useAddReaction();

  return (
    <Composer
      onComposerSubmit={({ body, attachments }, event) => {
        event.preventDefault();

        // Example mutations
        editComment({ threadId, commentId, body, attachments });
        addReaction({ threadId, commentId, emoji: "✅" });

        // Other custom behavior
        // ...
      }}
    />
  );
}
```

Learn more about mutation hooks under
[`@liveblocks/react`](/docs/api-reference/liveblocks-react#Comments).

##### Props [#Composer-props]

<PropertiesList>
  <PropertiesListItem name="threadId" type="string">
    The ID of the thread to reply to or to edit a comment in.
  </PropertiesListItem>
  <PropertiesListItem name="commentId" type="string">
    The ID of the comment to edit.
  </PropertiesListItem>
  <PropertiesListItem name="metadata" type="ThreadMetadata">
    The metadata of the thread to create.
  </PropertiesListItem>
  <PropertiesListItem
    name="commentMetadata"
    type="CommentMetadata | Partial<CommentMetadata>"
  >
    The metadata of the comment to create or edit.
  </PropertiesListItem>
  <PropertiesListItem
    name="onComposerSubmit"
    type="function"
    detailedType="(comment: ComposerSubmitComment, event: FormEvent<HTMLFormElement>) => Promise<void> | void"
  >
    The event handler called when the composer is submitted.
  </PropertiesListItem>
  <PropertiesListItem name="blurOnSubmit" type="boolean" defaultValue="true">
    Whether to blur the composer editor when the composer is submitted.
  </PropertiesListItem>
  <PropertiesListItem name="defaultValue" type="CommentBody">
    The composer’s initial value.
  </PropertiesListItem>
  <PropertiesListItem name="defaultAttachments" type="CommentAttachment[]">
    The composer’s initial attachments.
  </PropertiesListItem>
  <PropertiesListItem name="collapsed" type="boolean">
    Whether the composer is collapsed. Setting a value will make the composer
    controlled.
  </PropertiesListItem>
  <PropertiesListItem
    name="onCollapsedChange"
    type="function"
    detailedType="(collapsed: boolean) => void"
  >
    The event handler called when the collapsed state of the composer changes.
  </PropertiesListItem>
  <PropertiesListItem name="showAttachments" type="boolean" defaultValue="true">
    Whether to show and allow adding attachments.
  </PropertiesListItem>
  <PropertiesListItem
    name="showFormattingControls"
    type="boolean"
    defaultValue="true"
  >
    Whether to show formatting controls (e.g. a floating toolbar with formatting
    toggles when selecting text)
  </PropertiesListItem>
  <PropertiesListItem name="defaultCollapsed" type="boolean">
    Whether the composer is initially collapsed. Setting a value will make the
    composer uncontrolled.
  </PropertiesListItem>
  <PropertiesListItem name="disabled" type="boolean">
    Whether the composer is disabled.
  </PropertiesListItem>
  <PropertiesListItem name="autoFocus" type="boolean">
    Whether to focus the composer on mount.
  </PropertiesListItem>
  <PropertiesListItem
    name="overrides"
    type="Partial<GlobalOverrides & ComposerOverrides>"
  >
    Override the component’s strings.
  </PropertiesListItem>
</PropertiesList>

#### Comment

Displays a single comment.

```tsx
<Comment comment={comment} />
```

<Figure>
  <Image
    src="/assets/comments/comment.png"
    alt="Comment"
    width={768}
    height={446}
  />
</Figure>

Map through `thread.comments` to render each comment in a thread. Threads can be
retrieved with [`useThreads`](/docs/api-reference/liveblocks-react#useThreads).

```tsx highlight="9"
import { Comment } from "@liveblocks/react-ui";
import { ThreadData } from "@liveblocks/client";

// Renders a list of comments attach to the specified `thread`
function Component({ thread }: { thread: ThreadData }) {
  return (
    <>
      {thread.comments.map((comment) => (
        <Comment key={comment.id} comment={comment} />
      ))}
    </>
  );
}
```

##### Custom thread components

[`Comment`](#Comment) can be used in combination with [`Composer`](#Composer) to
create a custom thread component. The composer in this example is used to
[reply to the existing thread](/docs/api-reference/liveblocks-react-ui#Replying-to-a-thread).

```tsx highlight="10"
import { Comment, Composer } from "@liveblocks/react-ui";
import { ThreadData } from "@liveblocks/client";
import { useThreads } from "@liveblocks/react/suspense";

// Renders a list of comments and a composer for adding new comments
function CustomThread({ thread }: { thread: ThreadData }) {
  return (
    <>
      {thread.comments.map((comment) => (
        <Comment key={comment.id} comment={comment} />
      ))}
      <Composer threadId={thread.id} />
    </>
  );
}

// Renders a list of custom thread components
function Component() {
  const { threads } = useThreads();

  return (
    <>
      {threads.map((thread) => (
        <CustomThread key={thread.id} />
      ))}
    </>
  );
}
```

##### Props [#Comment-props]

<PropertiesList>
  <PropertiesListItem name="comment" type="CommentData" required>
    The comment to display.
  </PropertiesListItem>
  <PropertiesListItem
    name="showActions"
    type='boolean | "hover"'
    defaultValue='"hover"'
  >
    How to show or hide the actions.
  </PropertiesListItem>
  <PropertiesListItem name="showReactions" type="boolean" defaultValue="true">
    Whether to show reactions.
  </PropertiesListItem>
  <PropertiesListItem name="showAttachments" type="boolean" defaultValue="true">
    Whether to show attachments.
  </PropertiesListItem>
  <PropertiesListItem
    name="showComposerFormattingControls"
    type="boolean"
    defaultValue="true"
  >
    Whether to show the composer’s formatting controls when editing the comment.
  </PropertiesListItem>
  <PropertiesListItem name="indentContent" type="boolean" defaultValue="true">
    Whether to indent the comment’s content.
  </PropertiesListItem>
  <PropertiesListItem name="showDeleted" type="boolean" defaultValue="false">
    Whether to show the comment if it was deleted. If set to `false`, it will
    render deleted comments as `null`.
  </PropertiesListItem>
  <PropertiesListItem
    name="dropdownItems"
    type="ReactNode | (props) => ReactNode"
    detailedType="ReactNode | ((props: { children: ReactNode, comment: CommentData }) => ReactNode)"
  >
    Add (or change) items to display in the comment’s dropdown.
  </PropertiesListItem>
  <PropertiesListItem
    name="onCommentEdit"
    type="function"
    detailedType="(comment: CommentData) => void"
  >
    The event handler called when the comment is edited.
  </PropertiesListItem>
  <PropertiesListItem
    name="onCommentDelete"
    type="function"
    detailedType="(comment: CommentData) => void"
  >
    The event handler called when the comment is deleted.
  </PropertiesListItem>
  <PropertiesListItem
    name="onAuthorClick"
    type="function"
    detailedType="(userId: string, event: MouseEvent<HTMLElement>) => void"
  >
    The event handler called when clicking on the author.
  </PropertiesListItem>
  <PropertiesListItem
    name="onMentionClick"
    type="function"
    detailedType="(mention: MentionData, event: MouseEvent<HTMLElement>) => void"
  >
    The event handler called when clicking on a mention.
  </PropertiesListItem>
  <PropertiesListItem
    name="onAttachmentClick"
    type="function"
    detailedType="(args: CommentAttachmentArgs, event: MouseEvent<HTMLElement>) => void"
  >
    The event handler called when clicking on a comment’s attachment.
  </PropertiesListItem>
  <PropertiesListItem
    name="overrides"
    type="Partial<GlobalOverrides & CommentOverrides & ComposerOverrides>"
  >
    Override the component’s strings.
  </PropertiesListItem>
</PropertiesList>

### Primitives

Primitives are unstyled, headless components that can be used to create fully
custom commenting experiences. We have a
[primitives example](/examples/comments-primitives) highlighting how to use
them.

<Banner title="Using primitives with TypeScript">

If you run into the
`Cannot find module '@liveblocks/react-ui/primitives' or its corresponding type declarations`
error, you should update your `tsconfig.json`’s `moduleResolution`
[property](https://www.typescriptlang.org/tsconfig#moduleResolution) to
`"node16"` or `"nodenext"` (or `"bundler"` if you’re on TS >=5).

</Banner>

#### Composition

All primitives are composable; they forward their props and refs, merge their
classes and styles, and chain their event handlers.

Inspired by [Radix](https://www.radix-ui.com/) (and powered by its
[`Slot`](https://www.radix-ui.com/primitives/docs/utilities/slot) utility), most
of the primitives also support an `asChild` prop to replace the rendered element
by any provided child, and both set of props will be merged.

```tsx
import { Button } from "@/my-design-system";

// Use the default <button> element
<Composer.Submit disabled>Send</Composer.Submit>;

// Use an existing custom <Button> component
<Composer.Submit disabled asChild>
  <Button variant="primary">Send</Button>
</Composer.Submit>;
```

Learn more about this concept on
[Radix’s composition guide](https://www.radix-ui.com/primitives/docs/guides/composition).

#### Composer [#primitives-Composer]

Used to render a composer for creating, or editing, threads and comments.

```tsx
<Composer.Form>
  <Composer.AttachmentsDropArea />
  <Composer.Editor
    components={{
      Mention: () => <Composer.Mention />,
      MentionSuggestions: () => (
        <Composer.Suggestions>
          <Composer.SuggestionsList>
            <Composer.SuggestionsListItem />
          </Composer.SuggestionsList>
        </Composer.Suggestions>
      ),
      Link: () => <Composer.Link />,
    }}
  />
  <Composer.AttachFiles />
  <Composer.Submit />
</Composer.Form>
```

Combine with
[`useCreateThread`](/docs/api-reference/liveblocks-react#useCreateThread) to
render a composer that creates threads.

```tsx
import {
  Composer,
  CommentBodyLinkProps,
  CommentBodyMentionProps,
  ComposerEditorMentionSuggestionsProps,
  ComposerSubmitComment,
} from "@liveblocks/react-ui/primitives";
import { useCreateThread, useUser } from "@liveblocks/react/suspense";
import { FormEvent } from "react";

// Render a custom composer that creates a thread on submit
function MyComposer() {
  // +++
  const createThread = useCreateThread();
  // +++

  function handleComposerSubmit(
    { body, attachments }: ComposerSubmitComment,
    event: FormEvent<HTMLFormElement>
  ) {
    event.preventDefault();

    // Create a new thread
    // +++
    const thread = createThread({
      body,
      attachments,
      metadata: {},
    });
    // +++
  }

  return (
    // +++
    <Composer.Form onComposerSubmit={handleComposerSubmit}>
      <Composer.Editor
        components={{
          Mention,
          MentionSuggestions,
          Link,
        }}
      />
      <Composer.Submit>Create thread</Composer.Submit>
    </Composer.Form>
    // +++
  );
}

// Render a mention in the composer's editor, e.g. "@Emil Joyce"
function Mention({ mention }: CommentBodyMentionProps) {
  return <Comment.Mention>@{mention.id}</Comment.Mention>;
}

// Render a list of mention suggestions, used after typing "@" in the editor
function MentionSuggestions({
  mentions,
  selectedMentionId,
}: ComposerEditorMentionSuggestionsProps) {
  return (
    <Composer.Suggestions>
      <Composer.SuggestionsList>
        {mentions.map((mention) => {
          switch (mention.kind) {
            case "user":
              return (
                <UserMentionSuggestion key={mention.id} userId={mention.id} />
              );
            case "group":
              return (
                <GroupMentionSuggestion key={mention.id} groupId={mention.id} />
              );
          }
        })}
      </Composer.SuggestionsList>
    </Composer.Suggestions>
  );
}

// Render a single mention suggestion from a `userId`
function UserMentionSuggestion({ userId }: { userId: string }) {
  const { user } = useUser(userId);

  return (
    <Composer.SuggestionsListItem value={user.id}>
      <img src={user.avatar} alt={user.name} />
      {user.name}
    </Composer.SuggestionsListItem>
  );
}

// Render a single mention suggestion from a `groupId`
function GroupMentionSuggestion({ groupId }: { groupId: string }) {
  const { group } = useGroupInfo(groupId);

  return (
    <Composer.SuggestionsListItem value={group.id}>
      <img src={group.avatar} alt={group.name} />
      {group.name}
    </Composer.SuggestionsListItem>
  );
}

// Render a link in the composer's editor, e.g. "https://liveblocks.io"
function Link({ href, children }: CommentBodyLinkProps) {
  return <Comment.Link href={href}>{children}</Comment.Link>;
}
```

##### Composer.Form [#primitives-Composer.Form]

Surrounds the composer’s content and handles submissions. By default, no action
occurs when the composer is submitted. You must create your own mutations within
`onComposerSubmit` for
[creating threads](/docs/api-reference/liveblocks-react#useCreateThread),
[creating comments](/docs/api-reference/liveblocks-react#useCreateComment),
[editing comments](/docs/api-reference/liveblocks-react#useEditComment), etc.

```tsx
<Composer.Form
  onComposerSubmit={({ body, attachments }) => {
    // Mutate your comments
    // ...
  }}
>
  {/* ... */}
</Composer.Form>
```

<PropertiesList>
  <PropertiesListItem name="defaultAttachments" type="CommentAttachment[]">
    The composer’s initial attachments.
  </PropertiesListItem>
  <PropertiesListItem
    name="pasteFilesAsAttachments"
    type="boolean"
    defaultValue="false"
  >
    Whether to create attachments when pasting files into the editor.
  </PropertiesListItem>
  <PropertiesListItem
    name="preventUnsavedChanges"
    type="boolean"
    defaultValue="true"
  >
    When `preventUnsavedChanges` is set on your [Liveblocks
    client](/docs/api-reference/liveblocks-client#prevent-users-losing-unsaved-changes)
    on
    [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#prevent-users-losing-unsaved-changes),
    then closing a browser tab will be prevented when there are unsaved changes.
    By default, that will include draft text or attachments that are being
    uploaded via this composer, but not submitted yet. If you want to prevent
    unsaved changes with Liveblocks, but not for this composer, you can opt-out
    this composer instance by setting this prop to `false`.
  </PropertiesListItem>
  <PropertiesListItem name="blurOnSubmit" type="boolean" defaultValue="true">
    Whether to blur the editor when the form is submitted.
  </PropertiesListItem>
  <PropertiesListItem
    name="onComposerSubmit"
    type="function"
    detailedType="(comment: ComposerSubmitComment, event: FormEvent<HTMLFormElement>) => Promise<void> | void"
  >
    The event handler called when the form is submitted.
  </PropertiesListItem>
  <PropertiesListItem name="disabled" type="boolean" defaultValue="false">
    Whether the composer is disabled.
  </PropertiesListItem>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

##### Composer.Editor [#primitives-Composer.Editor]

Displays the composer’s editor.

```tsx
<Composer.Editor placeholder="Write a comment…" />
```

<PropertiesList>
  <PropertiesListItem name="defaultValue" type="CommentBody">
    The editor’s initial value.
  </PropertiesListItem>
  <PropertiesListItem name="placeholder" type="string">
    The text to display when the editor is empty.
  </PropertiesListItem>
  <PropertiesListItem name="disabled" type="boolean">
    Whether the editor is disabled.
  </PropertiesListItem>
  <PropertiesListItem name="autoFocus" type="boolean">
    Whether to focus the editor on mount.
  </PropertiesListItem>
  <PropertiesListItem name="dir" type='"ltr" | "rtl"'>
    The reading direction of the editor and related elements.
  </PropertiesListItem>
  <PropertiesListItem
    name="components"
    type="Partial<ComposerEditorComponents>"
  >
    The components displayed within the editor.
  </PropertiesListItem>
</PropertiesList>

<Table columns={["28%", "auto"]}>

| Attribute       | Value                                   |
| --------------- | --------------------------------------- |
| `data-focused`  | Present when the component is focused.  |
| `data-disabled` | Present when the component is disabled. |

</Table>

###### components [#primitives-Composer.Editor-components]

The components displayed within the editor.

<PropertiesList>
  <PropertiesListItem
    name="Mention"
    type="ComponentType<ComposerEditorMentionProps>"
  >
    The component used to display mentions. Defaults to the mention’s `id`
    prefixed by an @.
  </PropertiesListItem>
  <PropertiesListItem
    name="MentionSuggestions"
    type="ComponentType<ComposerEditorMentionSuggestionProps>"
  >
    The component used to display mention suggestions. Defaults to a list of the
    suggested mentions’ `id`.
  </PropertiesListItem>
  <PropertiesListItem name="Link" type="ComponentType<ComposerEditorLinkProps>">
    The component used to display links. Defaults to the link’s `children`
    property.
  </PropertiesListItem>
  <PropertiesListItem
    name="FloatingToolbar"
    type="ComponentType<ComposerEditorFloatingToolbarProps>"
  >
    The component used to display a floating toolbar attached to the selection.
  </PropertiesListItem>
</PropertiesList>

###### Mention [#primitives-Composer.Editor-Mention]

The component used to display mentions.

```tsx
<Composer.Editor
  components={{
    Mention: ({ mention, isSelected }) => (
      <Composer.Mention>@{mention.id}</Composer.Mention>
    ),
  }}
/>
```

<PropertiesList>
  <PropertiesListItem name="mention" type="MentionData">
    The mention to display.
  </PropertiesListItem>
  <PropertiesListItem name="isSelected" type="boolean">
    Whether the mention is selected.
  </PropertiesListItem>
</PropertiesList>

###### MentionSuggestions [#primitives-Composer.Editor-MentionSuggestions]

The component used to display mention suggestions.

<PropertiesList>
  <PropertiesListItem name="mentions" type="MentionData[]">
    The list of suggested mentions.
  </PropertiesListItem>
  <PropertiesListItem name="selectedMentionId" type="string">
    The currently selected mention’s ID.
  </PropertiesListItem>
</PropertiesList>

```tsx
<Composer.Editor
  components={{
    MentionSuggestions: () => (
      <Composer.Suggestions>
        <Composer.SuggestionsList>
          <Composer.SuggestionsListItem />
        </Composer.SuggestionsList>
      </Composer.Suggestions>
    ),
  }}
/>
```

###### Link [#primitives-Composer.Editor-Link]

The component used to display links.

```tsx
<Composer.Editor
  components={{
    Link: ({ href, children }) => <Composer.Link>{children}</Composer.Link>,
  }}
/>
```

<PropertiesList>
  <PropertiesListItem name="href" type="string">
    The link’s absolute URL.
  </PropertiesListItem>
  <PropertiesListItem name="children" type="ReactNode">
    The link’s content.
  </PropertiesListItem>
</PropertiesList>

###### FloatingToolbar [#primitives-Composer.Editor-FloatingToolbar]

Displays a floating toolbar attached to the selection within `Composer.Editor`.

```tsx
<Composer.Editor
  components={{
    FloatingToolbar: () => (
      <Composer.FloatingToolbar>
        <Composer.MarkToggle mark="bold">Bold</Composer.MarkToggle>
        <Composer.MarkToggle mark="italic">Italic</Composer.MarkToggle>
      </Composer.FloatingToolbar>
    ),
  }}
/>
```

##### Composer.Mention [#primitives-Composer.Mention]

Displays mentions within `Composer.Editor`.

```tsx
<Composer.Mention>@{mention.id}</Composer.Mention>
```

<PropertiesList>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

<Table columns={["28%", "auto"]}>

| Attribute       | Value                                 |
| --------------- | ------------------------------------- |
| `data-selected` | Present when the mention is selected. |

</Table>

##### Composer.Suggestions [#primitives-Composer.Suggestions]

Contains suggestions within `Composer.Editor`.

```tsx
<Composer.Suggestions>{/* ... */}<Composer.Suggestions>
```

<PropertiesList>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

##### Composer.SuggestionsList [#primitives-Composer.SuggestionsList]

Displays a list of suggestions within `Composer.Editor`.

```tsx
<Composer.SuggestionsList>
  {mentions.map((mention) => (
    <Composer.SuggestionsListItem key={mention.id} value={mention.id}>
      @{mention.id}
    </Composer.SuggestionsListItem>
  ))}
</Composer.SuggestionsList>
```

<PropertiesList>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

##### Composer.SuggestionsListItem [#primitives-Composer.SuggestionsListItem]

Displays a suggestion within `Composer.SuggestionsList`.

```tsx
<Composer.SuggestionsListItem key={mention.id} value={mention.id}>
  @{mention.id}
</Composer.SuggestionsListItem>
```

<PropertiesList>
  <PropertiesListItem name="value" type="string" required>
    The suggestion’s value.
  </PropertiesListItem>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

<Table columns={["28%", "auto"]}>

| Attribute       | Value                              |
| --------------- | ---------------------------------- |
| `data-selected` | Present when the item is selected. |

</Table>

##### Composer.Link [#primitives-Composer.Link]

Displays links within `Composer.Editor`.

```tsx
<Composer.Link href={href}>{children}</Composer.Link>
```

<PropertiesList>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

##### Composer.Submit [#primitives-Composer.Submit]

A button to submit the composer.

```tsx
<Composer.Submit>Send</Composer.Submit>
```

<PropertiesList>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

##### Composer.FloatingToolbar [#primitives-Composer.FloatingToolbar]

Displays a floating toolbar attached to the selection within `Composer.Editor`.

```tsx
<Composer.FloatingToolbar>
  <Composer.MarkToggle mark="bold">Bold</Composer.MarkToggle>
  <Composer.MarkToggle mark="italic">Italic</Composer.MarkToggle>
</Composer.FloatingToolbar>
```

<PropertiesList>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

##### Composer.MarkToggle

A toggle button which toggles a specific text mark.

```tsx
<Composer.MarkToggle mark="bold">Bold</Composer.MarkToggle>
```

<PropertiesList>
  <PropertiesListItem name="mark" type="ComposerBodyMark" required>
    The text mark to toggle.
  </PropertiesListItem>
  <PropertiesListItem
    name="onValueChange"
    type="function"
    detailedType="(mark: ComposerBodyMark) => void"
  >
    The event handler called when the mark is toggled.
  </PropertiesListItem>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

##### Composer.AttachFiles [#primitives-Composer.AttachFiles]

A button which opens a file picker to create attachments.

```tsx
<Composer.AttachFiles>Attach files</Composer.AttachFiles>
```

<PropertiesList>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

##### Composer.AttachmentsDropArea [#primitives-Composer.AttachmentsDropArea]

A drop area which accepts files to create attachments.

```tsx
<Composer.AttachmentsDropArea>Drop files here</Composer.AttachmentsDropArea>
```

<PropertiesList>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

#### Comment [#primitives-Comment]

Used to render a single comment.

```tsx
<Comment.Body
  components={{
    Mention: Comment.Mention,
    Link: Comment.Link,
  }}
/>
```

Map through `thread.comments` to render each comment in a thread. Threads can be
retrieved with [`useThreads`](/docs/api-reference/liveblocks-react#useThreads).

```tsx highlight="13-21"
import {
  Comment,
  CommentBodyLinkProps,
  CommentBodyMentionProps,
} from "@liveblocks/react-ui/primitives";
import { ThreadData } from "@liveblocks/client";

// Render custom comments in a thread. Pass a thread from `useThreads`.
function MyComments({ thread }: { thread: ThreadData }) {
  return (
    <>
      {thread.comments.map((comment) => (
        <div key={comment.id}>
          <Comment.Body
            body={comment.body}
            components={{
              Mention,
              Link,
            }}
          />
        </div>
      ))}
    </>
  );
}

// Render a mention in the comment, e.g. "@Emil Joyce"
function Mention({ mention }: CommentBodyMentionProps) {
  return <Comment.Mention>@{mention.id}</Comment.Mention>;
}

// Render a link in the comment, e.g. "https://liveblocks.io"
function Link({ href, children }: CommentBodyLinkProps) {
  return <Comment.Link href={href}>{children}</Comment.Link>;
}
```

##### Comment.Body [#primitives-Comment.Body]

Displays a comment body.

```tsx
<Comment.Body body={comment.body} />
```

<PropertiesList>
  <PropertiesListItem name="body" type="CommentBody">
    The comment body to display. If not defined, the component will render
    `null`.
  </PropertiesListItem>
  <PropertiesListItem name="components" type="Partial<CommentBodyComponents>">
    The components displayed within the comment body.
  </PropertiesListItem>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

###### components [#primitives-Comment.Body-components]

The components displayed within the comment body.

<PropertiesList>
  <PropertiesListItem
    name="Mention"
    type="ComponentType<CommentBodyMentionProps>"
  >
    The component used to display mentions. Defaults to the mention’s `id`
    prefixed by an @.
  </PropertiesListItem>
  <PropertiesListItem name="Link" type="ComponentType<CommentBodyLinkProps>">
    The component used to display links. Defaults to the link’s `children`
    property.
  </PropertiesListItem>
</PropertiesList>

###### Mention [#primitives-Comment.Body-Mention]

The component used to display mentions.

```tsx
<Comment.Body
  components={{
    Mention: ({ mention }) => <Comment.Mention>@{mention.id}</Comment.Mention>,
  }}
/>
```

<PropertiesList>
  <PropertiesListItem name="mention" type="MentionData">
    The mention to display.
  </PropertiesListItem>
</PropertiesList>

###### Link [#primitives-Comment.Body-Link]

The component used to display links.

```tsx
<Comment.Body
  components={{
    Link: ({ href, children }) => (
      <Comment.Link href={href}>{children}</Comment.Link>
    ),
  }}
/>
```

<PropertiesList>
  <PropertiesListItem name="href" type="string">
    The link’s absolute URL.
  </PropertiesListItem>
  <PropertiesListItem name="children" type="ReactNode">
    The link’s content.
  </PropertiesListItem>
</PropertiesList>

##### Comment.Mention [#primitives-Comment.Mention]

Displays mentions within `Comment.Body`.

```tsx
<Comment.Mention>@{mention.id}</Comment.Mention>
```

<PropertiesList>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

##### Comment.Link [#primitives-Comment.Link]

Displays links within `Comment.Body`.

```tsx
<Comment.Link href={href}>{children}</Comment.Link>
```

<PropertiesList>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

#### Timestamp [#primitives-Timestamp]

Displays a formatted date, and automatically re-renders to support relative
formatting. Defaults to relative formatting for nearby dates (e.g. “5 minutes
ago” or "in 1 day") and a short absolute formatting for more distant ones (e.g.
“25 Aug”).

```tsx
<Timestamp date={new Date()} />
```

Use with `comment.createdAt`, `comment.editedAt`, or `comment.deletedAt` to
display a human-readable time.

```tsx
import { Timestamp, Comment } from "@liveblocks/react-ui/primitives";
import { ThreadData } from "@liveblocks/client";

function MyComments({ thread }: { thread: ThreadData }) {
  return (
    <>
      {thread.comments.map((comment) => (
        <div key={comment.id}>
          // +++
          <Timestamp date={comment.createdAt} />
          // +++
          <Comment.Body body={comment.body} components={/* ... */} />
        </div>
      ))}
    </>
  );
}
```

<PropertiesList>
  <PropertiesListItem name="date" type="Date | string | number" required>
    The date to display.
  </PropertiesListItem>
  <PropertiesListItem
    name="children"
    type="function"
    detailedType="(date: Date) => ReactNode"
  >
    A function to format the displayed date. Defaults to a relative date
    formatting function.
  </PropertiesListItem>
  <PropertiesListItem
    name="title"
    type="string | function"
    detailedType="string | ((date: Date) => string)"
  >
    The `title` attribute’s value or a function to format it. Defaults to an
    absolute date formatting function.
  </PropertiesListItem>
  <PropertiesListItem
    name="interval"
    type="number | false"
    defaultValue="30000"
  >
    The interval in milliseconds at which the component will re-render. Can be
    set to `false` to disable re-rendering.
  </PropertiesListItem>
  <PropertiesListItem name="locale" type="string">
    The locale used when formatting the date. Defaults to the browser’s locale.
  </PropertiesListItem>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

#### Duration [#primitives-Duration]

Displays a formatted duration, and automatically re-renders to if the duration
is in progress. Defaults to a short format (e.g. “5s” or “1m 40s”).

```tsx
<Duration duration={3 * 60 * 1000} />
```

Instead of providing a duration in milliseconds, you can also provide start and
end dates for the duration via the `from` and `to` props. If only `from` is
provided it means that the duration is in progress, and the component will
re-render at an interval, customizable with the `interval` prop.

<PropertiesList>
  <PropertiesListItem name="duration" type="number">
    The duration in milliseconds. If provided, `from` and `to` will be ignored.
  </PropertiesListItem>
  <PropertiesListItem name="from" type="Date | string | number">
    The date at which the duration starts. If provided, `duration` will be
    ignored. If provided without `to` it means that the duration is in progress,
    and the component will re-render at an interval, customizable with the
    `interval` prop.
  </PropertiesListItem>
  <PropertiesListItem name="to" type="Date | string | number">
    The date at which the duration ends. If `from` is provided without `to`,
    `Date.now()` will be used.
  </PropertiesListItem>
  <PropertiesListItem
    name="children"
    type="function"
    detailedType="(duration: number) => ReactNode"
  >
    A function to format the displayed date. Defaults to a short duration
    formatting function.
  </PropertiesListItem>
  <PropertiesListItem
    name="title"
    type="string | function"
    detailedType="string | ((duration: number) => string)"
  >
    The `title` attribute’s value or a function to format it. Defaults to an
    longer duration formatting function.
  </PropertiesListItem>
  <PropertiesListItem name="interval" type="number | false" defaultValue="500">
    The interval in milliseconds at which the component will re-render if `from`
    is provided without `to`, meaning that the duration is in progress. Can be
    set to `false` to disable re-rendering.
  </PropertiesListItem>
  <PropertiesListItem name="locale" type="string">
    The locale used when formatting the duration. Defaults to the browser’s
    locale.
  </PropertiesListItem>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

#### FileSize [#primitives-FileSize]

Displays a formatted file size.

```tsx
<FileSize size={100000} />
```

Use with `attachment.size` to display a human-readable file size.

```tsx
import { FileSize } from "@liveblocks/react-ui/primitives";
import { CommentData } from "@liveblocks/client";

function MyComment({ comment }: { comment: CommentData }) {
  return (
    <div>
      {/* ... */}

      {comment.attachments.map((attachment) => (
        <div key={attachment.id}>
          {attachment.name}
          // +++
          <FileSize size={attachment.size} />
          // +++
        </div>
      ))}
    </div>
  );
}
```

<PropertiesList>
  <PropertiesListItem name="size" type="number" required>
    The file size to display.
  </PropertiesListItem>
  <PropertiesListItem
    name="children"
    type="function"
    detailedType="(size: number) => ReactNode"
  >
    A function to format the displayed file size. Defaults to a human-readable
    file size formatting function.
  </PropertiesListItem>
  <PropertiesListItem name="locale" type="string">
    The locale used when formatting the file size. Defaults to the browser’s
    locale.
  </PropertiesListItem>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

#### Emoji picker [#emoji-picker]

Using [Frimousse](https://frimousse.liveblocks.io) alongside
[`useAddReaction`](/docs/api-reference/liveblocks-react#useAddReaction), a
package originally designed for Comments, you can easily add an emoji picker to
your primitive Comments components.

```tsx
import { EmojiPicker } from "frimousse";
import { useAddReaction } from "@liveblocks/react/suspense";
import { CommentData } from "@liveblocks/client";

export function MyEmojiPicker({ comment }: { comment: CommentData }) {
  const addReaction = useAddReaction();

  return (
    <EmojiPicker.Root
      onEmojiSelect={({ emoji }) => {
        addReaction({
          threadId: comment.threadId,
          commentId: comment.id,
          emoji,
        });
      }}
    >
      <EmojiPicker.Search />
      <EmojiPicker.Viewport>
        <EmojiPicker.Loading>Loading…</EmojiPicker.Loading>
        <EmojiPicker.Empty>No emoji found.</EmojiPicker.Empty>
        <EmojiPicker.List />
      </EmojiPicker.Viewport>
    </EmojiPicker.Root>
  );
}
```

Find a full code snippet of this in our
[Comments primitives example](/examples/comments-primitives/nextjs-comments-primitives).

#### Emoji reactions [#emoji-reactions]

A list of clickable emoji reactions can be created using the
[`useAddReaction`](/docs/api-reference/liveblocks-react#useAddReaction),
[`useRemoveReaction`](/docs/api-reference/liveblocks-react#useRemoveReaction),
and [`useSelf`](/docs/api-reference/liveblocks-react#useSelf) hooks.

```tsx
import { CommentData } from "@liveblocks/client";
import {
  useAddReaction,
  useRemoveReaction,
  useSelf,
} from "@liveblocks/react/suspense";

export function MyEmojiReactions({ comment }: { comment: CommentData }) {
  const userId = useSelf().id;
  const addReaction = useAddReaction();
  const removeReaction = useRemoveReaction();

  return (
    <>
      {comment.reactions.map((reaction) => {
        const hasPicked = reaction.users.some((user) => user.id === userId);
        const reactionObject = {
          threadId: comment.threadId,
          commentId: comment.id,
          emoji: reaction.emoji,
        };

        return (
          <button
            key={reaction.emoji}
            onClick={() =>
              hasPicked
                ? removeReaction(reactionObject)
                : addReaction(reactionObject)
            }
            data-picked={hasPicked || undefined /* Use for CSS styling */}
          >
            {reaction.emoji} {reaction.users.length}
          </button>
        );
      })}
    </>
  );
}
```

### Hooks

#### useComposer

Returns states and methods related to the composer. Can only be used within the
[`Composer.Form`](#primitives-Composer.Form) primitive.
[All values listed below](#useComposer-values).

```tsx
import { useComposer } from "@liveblocks/react-ui/primitives";

const { isEmpty, attachments, submit /* ... */ } = useComposer();
```

##### Custom composer behavior

`useComposer` can be used in combination with
[`Composer` primitives](#primitives-Composer) to create a custom composer, and
control its behavior. For example, `createMention` allows you to create a button
which focuses the editor, adds `@`, and opens the mention suggestions dropdown.

```tsx
import { Composer, useComposer } from "@liveblocks/react-ui/primitives";
import { useCreateThread } from "@liveblocks/react/suspense";

function MyComposer() {
  const createThread = useCreateThread();

  return (
    <Composer.Form
      onComposerSubmit={({ body, attachments }) => {
        const thread = createThread({
          body,
          attachments,
          metadata: {},
        });
      }}
    >
      <Editor />
    </Composer.Form>
  );
}

function Editor() {
  // +++
  const { createMention } = useComposer();
  // +++

  return (
    <>
      <Composer.Editor components={/* Your custom component parts */} />
      // +++
      <button onClick={createMention}>Add mention</button>
      // +++
    </>
  );
}
```

##### Handle attachments

When using primitives,
[`Composer.AttachFiles`](#primitives-Composer.AttachFiles) and
[`Composer.AttachmentsDropArea`](#primitives-Composer.AttachmentsDropArea) add
attachments to the composer, but they’re not rendered without `useComposer`. The
`attachments` array can be used to render the current attachments, and
`removeAttachment` allows you to remove them.

```tsx
import { Composer, useComposer } from "@liveblocks/react-ui/primitives";
import { useCreateThread } from "@liveblocks/react/suspense";

function MyComposer() {
  const createThread = useCreateThread();

  return (
    <Composer.Form
      onComposerSubmit={({ body, attachments }) => {
        const thread = createThread({
          body,
          attachments,
          metadata: {},
        });
      }}
    >
      <Composer.Editor components={/* Your custom component parts */} />
      // +++
      <MyComposerAttachments />
      // +++
      <Composer.AttachFiles>Attach Files</Composer.AttachFiles>
      <Composer.Submit>Submit</Composer.Submit>
    </Composer.Form>
  );
}

function MyComposerAttachments() {
  // +++
  const { attachments, removeAttachment } = useComposer();
  // +++

  return (
    <div>
      // +++
      {attachments.map((attachment) => (
        <div key={attachment.id}>
          {attachment.name} ({attachment.status})
          <button onClick={() => removeAttachment(attachment.id)}>
            Remove
          </button>
        </div>
      ))}
      // +++
    </div>
  );
}
```

##### Values [#useComposer-values]

<PropertiesList>
  <PropertiesListItem name="isDisabled" type="boolean">
    Whether the composer is currently disabled.
  </PropertiesListItem>
  <PropertiesListItem name="isFocused" type="boolean">
    Whether the editor is currently focused.
  </PropertiesListItem>
  <PropertiesListItem name="isEmpty" type="boolean">
    Whether the editor is currently empty.
  </PropertiesListItem>
  <PropertiesListItem name="canSubmit" type="boolean">
    Whether the composer can currently be submitted.
  </PropertiesListItem>
  <PropertiesListItem name="submit" type="function" detailedType="() => void">
    Submit the editor programmatically.
  </PropertiesListItem>
  <PropertiesListItem name="clear" type="function" detailedType="() => void">
    Clear the editor programmatically.
  </PropertiesListItem>
  <PropertiesListItem name="select" type="function" detailedType="() => void">
    Select the editor programmatically.
  </PropertiesListItem>
  <PropertiesListItem name="focus" type="function" detailedType="() => void">
    Focus the editor programmatically.
  </PropertiesListItem>
  <PropertiesListItem name="blur" type="function" detailedType="() => void">
    Blur the editor programmatically.
  </PropertiesListItem>
  <PropertiesListItem name="marks" type="ComposerBodyMarks">
    Which text marks are currently active and which aren’t.
  </PropertiesListItem>
  <PropertiesListItem
    name="toggleMark"
    type="function"
    detailedType="(mark: ComposerBodyMark) => void"
  >
    Toggle a specific text mark.
  </PropertiesListItem>
  <PropertiesListItem
    name="createMention"
    type="function"
    detailedType="() => void"
  >
    Start creating a mention at the current selection.
  </PropertiesListItem>
  <PropertiesListItem
    name="insertText"
    type="function"
    detailedType="(text: string) => void"
  >
    Insert text at the current selection.
  </PropertiesListItem>
  <PropertiesListItem
    name="attachFiles"
    type="function"
    detailedType="() => void"
  >
    Open a file picker programmatically to create attachments.
  </PropertiesListItem>
  <PropertiesListItem name="attachments" type="ComposerAttachment[]">
    The composer’s current attachments.
  </PropertiesListItem>
  <PropertiesListItem
    name="removeAttachment"
    type="function"
    detailedType="(attachmentId: string) => void"
  >
    Remove an attachment by its ID.
  </PropertiesListItem>
</PropertiesList>

#### Other hooks

Other Comments hooks are part of
[`@liveblocks/react`](/docs/api-reference/liveblocks-react), you can find them
on the
[React API reference page](/docs/api-reference/liveblocks-react#Comments).

- [`useThreads`](/docs/api-reference/liveblocks-react#useThreads)
- [`useThreadSubscription`](/docs/api-reference/liveblocks-react#useThreadSubscription)
- [`useCreateThread`](/docs/api-reference/liveblocks-react#useCreateThread)
- [`useDeleteThread`](/docs/api-reference/liveblocks-react#useDeleteThread)
- [`useEditThreadMetadata`](/docs/api-reference/liveblocks-react#useEditThreadMetadata)
- [`useMarkThreadAsResolved`](/docs/api-reference/liveblocks-react#useMarkThreadAsResolved)
- [`useMarkThreadAsUnresolved`](/docs/api-reference/liveblocks-react#useMarkThreadAsUnresolved)
- [`useMarkThreadAsRead`](/docs/api-reference/liveblocks-react#useMarkThreadAsRead)
- [`useCreateComment`](/docs/api-reference/liveblocks-react#useCreateComment)
- [`useEditComment`](/docs/api-reference/liveblocks-react#useEditComment)
- [`useEditCommentMetadata`](/docs/api-reference/liveblocks-react#useEditCommentMetadata)
- [`useDeleteComment`](/docs/api-reference/liveblocks-react#useDeleteComment)
- [`useAddReaction`](/docs/api-reference/liveblocks-react#useAddReaction)
- [`useRemoveReaction`](/docs/api-reference/liveblocks-react#useRemoveReaction)
- [`useAttachmentUrl`](/docs/api-reference/liveblocks-react#useAttachmentUrl)

## Notifications

### Default components

#### InboxNotification

Displays a single inbox notification.

```tsx
<InboxNotification inboxNotification={inboxNotification} />
```

<Figure>
  <Image
    src="/assets/notifications/inbox-notification.png"
    alt="InboxNotification"
    width={768}
    height={446}
  />
</Figure>

Map through `inboxNotifications` with
[`useInboxNotifications`](/docs/api-reference/liveblocks-react#useInboxNotifications)
to render a list of the room’s notifications.

```tsx highlight="10-13"
import { InboxNotification } from "@liveblocks/react-ui";
import { useInboxNotifications } from "@liveblocks/react/suspense";

function Component() {
  const { inboxNotifications } = useInboxNotifications();

  return (
    <>
      {inboxNotifications.map((inboxNotification) => (
        <InboxNotification
          key={inboxNotification.id}
          inboxNotification={inboxNotification}
        />
      ))}
    </>
  );
}
```

##### Rendering notification kinds differently

Different `kinds` of notifications are available, for example `thread` which is
triggered when using Comments, or `$myCustomNotification` which would be a
custom notification you’ve triggered manually. You can choose to render each
notification differently.

```tsx highlight="4-14"
<InboxNotification
  inboxNotification={inboxNotification}
  kinds={{
    thread: (props) => (
      <InboxNotification.Thread {...props} showRoomName={false} />
    ),
    $myCustomNotification: (props) => (
      <InboxNotification.Custom
        {...props}
        title="New notification"
        aside={<InboxNotification.Icon>❕</InboxNotification.Icon>}
      >
        My custom notification
      </InboxNotification.Custom>
    ),
  }}
/>
```

Adding these two properties to `kinds` will overwrite the default component
that’s displayed for those two notification types. Using
[`InboxNotification.Thread`](/docs/api-reference/liveblocks-react-ui#InboxNotification.Thread)
and
[`InboxNotification.Custom`](/docs/api-reference/liveblocks-react-ui#InboxNotification.Custom)
in this way allow you to easily create components that fit into the existing
design system, whilst still adding lots of customization. However, it’s also
valid to render any custom JSX.

```tsx highlight="3-5"
<InboxNotification
  inboxNotification={inboxNotification}
  kinds={{
    $myCustomNotification: (props) => <div>New notification</div>,
  }}
/>
```

##### Typing custom notifications

To type custom notifications, edit the `ActivitiesData` type in your config
file.

```ts file="liveblocks.config.ts" highlight="4-10"
declare global {
  interface Liveblocks {
    // Custom activities data for custom notification kinds
    ActivitiesData: {
      // Example, a custom $alert kind
      $alert: {
        title: string;
        message: string;
      };
    };

    // Other kinds
    // ...
  }
}
```

Your activities data is now correctly typed in inline functions.

```tsx highlight="5-6"
<InboxNotification
  inboxNotification={inboxNotification}
  kinds={{
    $alert: (props) => {
      // `title` and `message` are correctly typed, as defined in your config
      const { title, message } = props.inboxNotification.activities[0].data;

      return (
        <InboxNotification.Custom
          {...props}
          title={title}
          aside={<InboxNotification.Icon>❗</InboxNotification.Icon>}
        >
          {message}
        </InboxNotification.Custom>
      );
    },
  }}
/>
```

If you’d like to create a typed function elsewhere, you can use
`InboxNotificationCustomProps` with a generic. In the example below we’re using
the `$alert` notification kind as a generic,
`InboxNotificationCustomKindProps<"$alert">`.

```tsx highlight="6-8,25"
import {
  InboxNotification,
  InboxNotificationCustomKindProps,
} from "@liveblocks/react-ui";

function AlertNotification(props: InboxNotificationCustomKindProps<"$alert">) {
  // `title` and `message` are correctly typed, as defined in your config
  const { title, message } = props.inboxNotification.activities[0].data;

  return (
    <InboxNotification.Custom
      {...props}
      title={title}
      aside={<InboxNotification.Icon>❗</InboxNotification.Icon>}
    >
      {message}
    </InboxNotification.Custom>
  );
}

function Notification({ inboxNotification }) {
  return (
    <InboxNotification
      inboxNotification={inboxNotification}
      kinds={{ $alert: AlertNotification }}
    />
  );
}
```

##### Batching custom notifications

If you’re
[batching custom notifications](/docs/api-reference/liveblocks-node#Batching-custom-notifications),
you can render each activity inside a single notification.

```tsx
<InboxNotification
  inboxNotification={inboxNotification}
  kinds={{
    $alert: (props) => {
      // Each batched `activityData` is added to the `activities` array
      const { activities } = props.inboxNotification;

      return (
        <InboxNotification.Custom
          {...props}
          title={title}
          aside={<InboxNotification.Icon>❗</InboxNotification.Icon>}
        >
          {activities.map((activity) => (
            <div key={activity.id}>
              <div>{activity.data.title}</div>
              <div>{activity.data.message}</div>
            </div>
          ))}
        </InboxNotification.Custom>
      );
    },
  }}
/>
```

##### Props [#InboxNotification-props]

<PropertiesList>
  <PropertiesListItem
    name="inboxNotification"
    type="InboxNotificationData"
    required
  >
    The inbox notification to display.
  </PropertiesListItem>
  <PropertiesListItem name="href" type="string">
    The URL which the inbox notification links to.
  </PropertiesListItem>
  <PropertiesListItem name="showActions" type='boolean | "hover"'>
    How to show or hide the actions.
  </PropertiesListItem>
  <PropertiesListItem name="kinds" type="Partial<InboxNotificationKinds>">
    Override specific kinds of inbox notifications.
  </PropertiesListItem>
  <PropertiesListItem
    name="overrides"
    type="Partial<GlobalOverrides & InboxNotificationOverrides & CommentOverrides>"
  >
    Override the component’s strings.
  </PropertiesListItem>
  <PropertiesListItem name="components" type="Partial<GlobalComponents>">
    Override the component’s components.
  </PropertiesListItem>
</PropertiesList>

###### kinds [#InboxNotification-kinds]

Override specific kinds of inbox notifications.

<PropertiesList>
  <PropertiesListItem
    name="thread"
    type="ComponentType<InboxNotificationThreadKindProps>"
  >
    The component used to display thread notifications. Defaults to
    `InboxNotification.Thread`.
  </PropertiesListItem>
  <PropertiesListItem
    name="textMention"
    type="ComponentType<InboxNotificationTextMentionKindProps>"
  >
    The component used to display text mention notifications. Defaults to
    `InboxNotification.TextMention`.
  </PropertiesListItem>
  <PropertiesListItem
    name="$${string}"
    type="ComponentType<InboxNotificationCustomKindProps>"
  >
    The component used to display a custom notification kind. Custom
    notification kinds must start with a `$`.
  </PropertiesListItem>
</PropertiesList>

###### InboxNotification.Thread [#InboxNotification.Thread]

Displays a thread inbox notification kind.

```tsx
<InboxNotification
  inboxNotification={inboxNotification}
  kinds={{
    thread: (props) => (
      <InboxNotification.Thread {...props} showRoomName={false} />
    ),
  }}
/>
```

<PropertiesList>
  <PropertiesListItem
    name="inboxNotification"
    type="InboxNotificationThreadData"
    required
  >
    The inbox notification to display.
  </PropertiesListItem>
  <PropertiesListItem name="showActions" type='boolean | "hover"'>
    How to show or hide the actions.
  </PropertiesListItem>
  <PropertiesListItem name="showRoomName" type="boolean" defaultValue="true">
    Whether to show the room name in the title.
  </PropertiesListItem>
  <PropertiesListItem name="showReactions" type="boolean" defaultValue="true">
    Whether to show reactions.
  </PropertiesListItem>
  <PropertiesListItem name="showAttachments" type="boolean" defaultValue="true">
    Whether to show attachments.
  </PropertiesListItem>
</PropertiesList>

###### InboxNotification.TextMention [#InboxNotification.TextMention]

Displays a text mention inbox notification kind.

```tsx
<InboxNotification
  inboxNotification={inboxNotification}
  kinds={{
    textMention: (props) => (
      <InboxNotification.TextMention {...props} showRoomName={false} />
    ),
  }}
/>
```

<PropertiesList>
  <PropertiesListItem
    name="inboxNotification"
    type="InboxNotificationTextMentionData"
    required
  >
    The inbox notification to display.
  </PropertiesListItem>
  <PropertiesListItem name="showActions" type='boolean | "hover"'>
    How to show or hide the actions.
  </PropertiesListItem>
  <PropertiesListItem name="showRoomName" type="boolean" defaultValue="true">
    Whether to show the room name in the title.
  </PropertiesListItem>
</PropertiesList>

###### InboxNotification.Custom [#InboxNotification.Custom]

Displays a custom notification kind.

```tsx
<InboxNotification
  inboxNotification={inboxNotification}
  kinds={{
    $myCustomNotificationKind: (props) => {
      const activityData = props.inboxNotification.activities[0].data;

      return (
        <InboxNotification.Custom
          title={
            <>
              User <strong>{activityData.file}</strong>
            </>
          }
          aside={<InboxNotification.Icon>❌</InboxNotification.Icon>}
          {...props}
        >
          {activityData.errorDescription}
        </InboxNotification.Custom>
      );
    },
  }}
/>
```

<PropertiesList>
  <PropertiesListItem
    name="inboxNotification"
    type="InboxNotificationCustomData"
    required
  >
    The inbox notification to display.
  </PropertiesListItem>
  <PropertiesListItem name="title" type="ReactNode" required>
    The inbox notification’s title.
  </PropertiesListItem>
  <PropertiesListItem name="children" type="ReactNode" required>
    The inbox notification’s content.
  </PropertiesListItem>
  <PropertiesListItem name="showActions" type='boolean | "hover"'>
    How to show or hide the actions.
  </PropertiesListItem>
  <PropertiesListItem name="aside" type="ReactNode">
    The inbox notification’s aside content. Can be combined with{" "}
    <code>InboxNotification.Icon</code> or <code>InboxNotification.Avatar</code>{" "}
    to easily follow default styles.
  </PropertiesListItem>
  <PropertiesListItem name="asChild" type="boolean" defaultValue="false">
    Replace the rendered element by the one passed as a child.
  </PropertiesListItem>
</PropertiesList>

###### InboxNotification.Inspector [#InboxNotification.Inspector]

Displays the inbox notification’s data in a JSON code snippet. Useful when
debugging notifications in your app.

```tsx
<InboxNotification
  inboxNotification={inboxNotification}
  kinds={{
    $myCustomNotificationKind: InboxNotification.Inspector,
  }}
/>
```

<PropertiesList>
  <PropertiesListItem
    name="inboxNotification"
    type="InboxNotificationData"
    required
  >
    The inbox notification to display.
  </PropertiesListItem>
  <PropertiesListItem name="showActions" type='boolean | "hover"'>
    How to show or hide the actions.
  </PropertiesListItem>
</PropertiesList>

#### InboxNotificationList

Displays inbox notifications as a list. Each
[`InboxNotification`](#InboxNotification) component will be wrapped in a `li`
element.

```tsx
<InboxNotificationList>
  <InboxNotification />
  <InboxNotification />
  <InboxNotification />
</InboxNotificationList>
```

<Figure>
  <Image
    src="/assets/notifications/inbox-notification-list.png"
    alt="InboxNotificationList"
    width={768}
    height={446}
  />
</Figure>

##### Props [#InboxNotificationList-props]

<PropertiesList>
  <PropertiesListItem name="children" type="ReactNode">
    The inbox notifications to display.
  </PropertiesListItem>
</PropertiesList>

### Hooks [#Notification-hooks]

All hooks for Notifications are in
[`@liveblocks/react`](/docs/api-reference/liveblocks-react#Notifications).

## Version History

Version history enables you to track and restore versions of your
[Lexical](https://liveblocks.io/docs/api-reference/liveblocks-react-lexical) or
[Yjs](/docs/api-reference/liveblocks-yjs) document. Versions can be
automatically created when enabled in your project settings, or manually created
using the
[REST API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-version).
These components aid in displaying a list of those versions.

### Default components

### HistoryVersionSummary

Displays a version summary which includes the author and date.

```tsx
<HistoryVersionSummary
  onClick={() => {
    setSelectedVersionId(version.id);
  }}
  version={version}
  selected={version.id === selectedVersionId}
/>
```

##### Props [#HistoryVersionSummary-props]

<PropertiesList>
  <PropertiesListItem name="onClick" type="() => void">
    The function to call when the version summary is clicked.
  </PropertiesListItem>
  <PropertiesListItem name="version" type="Version">
    The version object containing information about the specific version.
  </PropertiesListItem>
  <PropertiesListItem name="selected" type="boolean">
    Whether this version is currently selected.
  </PropertiesListItem>
</PropertiesList>

#### HistoryVersionSummaryList

Displays a list of version summaries for a document’s history including authors
and dates.

```tsx
<HistoryVersionSummaryList>
  {versions?.map((version) => (
    <HistoryVersionSummary
      onClick={() => {
        setSelectedVersionId(version.id);
      }}
      key={version.id}
      version={version}
      selected={version.id === selectedVersionId}
    />
  ))}
</HistoryVersionSummaryList>
```

##### Props [#HistoryVersionSummaryList-props]

<PropertiesList>
  <PropertiesListItem name="children" type="ReactNode">
    The version summaries to display, typically an array of
    HistoryVersionSummary components.
  </PropertiesListItem>
</PropertiesList>

## Utilities

### Components [#utilities-components]

#### Icon

Most icons used in the default components can be exported via `<Icon.* />`.
They’re stroke-based and designed for use at 20×20 pixels.

```tsx
import { Icon } from "@liveblocks/react-ui";

<Icon.QuestionMark />
<Icon.Mention  />
```

Find a full list of available icons in
[our GitHub repo](https://github.com/liveblocks/liveblocks/blob/main/packages/liveblocks-react-ui/src/icon.ts).

#### LiveblocksUiConfig

Set configuration options for all `@liveblocks/react-ui` components, such as
[overrides](#overrides).

```tsx
<LiveblocksUiConfig overrides={{ locale: "fr", USER_UNKNOWN: "Anonyme" }} />
```

##### Props [#LiveblocksUiConfig-props]

<PropertiesList>
  <PropertiesListItem name="overrides" type="Partial<Overrides>">
    Override the components’ strings.
  </PropertiesListItem>
  <PropertiesListItem name="components" type="Partial<Components>">
    Override the components’ components.
  </PropertiesListItem>
  <PropertiesListItem
    name="portalContainer"
    type="HTMLElement"
    defaultValue="document.body"
  >
    The container to render the portal into.
  </PropertiesListItem>
  <PropertiesListItem
    name="preventUnsavedComposerChanges"
    type="boolean"
    defaultValue="true"
  >
    When `preventUnsavedChanges` is set on your Liveblocks client (or set on
    `LiveblocksProvider`), then closing a browser tab will be prevented when
    there are unsaved changes. By default, that will include draft texts or
    attachments that are (being) uploaded via comments/threads composers, but
    not submitted yet. If you want to prevent unsaved changes with Liveblocks,
    but not for composers, you can opt-out by setting this option to `false`.
  </PropertiesListItem>
  <PropertiesListItem
    name="emojibaseUrl"
    type="string"
    defaultValue={`"https://cdn.jsdelivr.net/npm/emojibase-data"`}
  >
    Use this option to host your own emoji data.

    The Liveblocks emoji picker (visible when adding reactions in `Comment`) is built with
    [Frimousse](https://github.com/liveblocks/frimousse), which fetches its data
    from [Emojibase](https://emojibase.dev/docs/datasets/).

    This option allows you to change the base URL of where the
    [`emojibase-data`](https://www.npmjs.com/package/emojibase-data) files
    should be fetched from, used as follows: `${emojibaseUrl}/${locale}/${file}.json`.
    (e.g. `${emojibaseUrl}/en/data.json`).

  </PropertiesListItem>
</PropertiesList>

### Hooks [#Version-History-hooks]

All hooks for Version History are in
[`@liveblocks/react`](/docs/api-reference/liveblocks-react#Version-History).

## Styling and customization

### Default styles

The default components come with default styles. These styles can be imported
into the root of your app or directly into a CSS file with `@import`.

```tsx
import "@liveblocks/react-ui/styles.css";
```

### Dark mode

You can also import default dark mode styling. There are two versions to choose
from, the first uses the
[system theme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme).

```tsx
// Dark mode using the system theme with `prefers-color-scheme`
import "@liveblocks/react-ui/styles/dark/media-query.css";
```

The second uses the `dark` class name, and two commonly used data attributes.

```tsx
// Dark mode using `className="dark"`, `data-theme="dark"`, or `data-dark="true"`
import "@liveblocks/react-ui/styles/dark/attributes.css";
```

### CSS variables

The default components are built around a set of customizable CSS variables. Set
these variables within `.lb-root` to globally style your components.

```css
/* Styles all default Comments components */
.lb-root {
  --lb-accent: purple;
  --lb-spacing: 1em;
  --lb-radius: 0;
}
```

<PropertiesList>
  <PropertiesListItem name="--lb-radius" defaultValue="0.5em">
    The border radius scale. `em` values recommended.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-spacing" defaultValue="1em">
    The spacing scale. `em` values recommended.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-accent" defaultValue="#1177ff">
    The accent color.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-accent-foreground" defaultValue="#ffffff">
    The foreground color used over the accent color.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-destructive" defaultValue="#ff4455">
    The destructive color.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-destructive-foreground" defaultValue="#ffffff">
    The foreground color used over the destructive color.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-background" defaultValue="#ffffff">
    The main background color.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-foreground" defaultValue="#111111">
    The main foreground color.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-line-height" defaultValue="1.5">
    The line height of main elements (e.g. comment bodies).
  </PropertiesListItem>
  <PropertiesListItem name="--lb-icon-size" defaultValue="20px">
    The size of icons.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-icon-weight" defaultValue="1.5px">
    The stroke weight of icons.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-avatar-radius" defaultValue="50%">
    The border radius used for avatars.
  </PropertiesListItem>
  <PropertiesListItem
    name="--lb-button-radius"
    defaultValue="calc(0.75 * var(--lb-radius))"
  >
    The border radius used for buttons.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-transition-duration" defaultValue="0.1s">
    The duration used for transitioned elements.
  </PropertiesListItem>
  <PropertiesListItem
    name="--lb-transition-easing"
    defaultValue="cubic-bezier(0.4, 0, 0.2, 1)"
  >
    The easing function used for transitioned elements.
  </PropertiesListItem>
  <PropertiesListItem
    name="--lb-elevation-shadow"
    defaultValue="0 0 0 1px rgb(0 0 0 / 4%), 0 2px 6px rgb(0 0 0 / 8%),  0 8px 26px rgb(0 0 0 / 12%)"
  >
    The box shadow added to elevated elements.
  </PropertiesListItem>
  <PropertiesListItem
    name="--lb-elevation-shadow-moderate"
    defaultValue="  0 0 0 1px rgb(0 0 0 / 4%), 0 2px 6px rgb(0 0 0 / 6%), 0 8px 26px rgb(0 0 0 / 8%)"
  >
    The box shadow added to moderately elevated elements.
  </PropertiesListItem>
  <PropertiesListItem
    name="--lb-tooltip-shadow"
    defaultValue="0 2px 4px rgb(0 0 0 / 8%), 0 4px 12px rgb(0 0 0 / 12%)"
  >
    The box shadow added to tooltips.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-accent-contrast" defaultValue="8%">
    Affects the lightness of accent colors. `%` value required.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-destructive-contrast" defaultValue="8%">
    Affects the lightness of destructive colors. `%` value required.
  </PropertiesListItem>
  <PropertiesListItem name="--lb-foreground-contrast" defaultValue="6%">
    Affects the lightness of foreground colors. `%` value required.
  </PropertiesListItem>
</PropertiesList>

{/* TODO: Explain automatic color scales (with a palette-type visual) */}

### Class names

Each default component has a set of predefined class names, which can be helpful
for custom styling, for example.

```css
.lb-thread {
  /* Customise thread */
}

.lb-composer {
  /* Customise composer */
}
```

Additionally, some elements also have data attributes to provide contextual
information, for example:

```css
.lb-button[data-variant="primary"] {
  /* Customise primary buttons */
}

.lb-avatar[data-loading] {
  /* Customise avatar loading state */
}
```

<Banner title="Internal classes" type="warning">

Classes containing colons `:` are internal and may change over time.

</Banner>

### Portaled elements

Floating elements within the default components (e.g. tooltips, dropdowns, etc)
are portaled to the end of the document to avoid `z-index` conflicts and
`overflow` issues.

When portaled, those elements are also wrapped in a container to handle their
positioning. These containers don’t have any specific class names or data
attributes so they shouldn’t be targeted or styled directly, but they will
mirror whichever `z-index` value is set on their inner element (which would be
`auto` by default). So if you need to set a specific `z-index` value on floating
elements, you should set it on the floating elements themselves directly,
ignoring their containers. You can either target specific floating elements
(e.g. `.lb-tooltip`, `.lb-dropdown`, etc) or all of them at once via the
`.lb-portal` class name.

```css
/* Target all floating elements */
.lb-portal {
  z-index: 5;
}

/* Target a specific floating element */
.lb-tooltip {
  z-index: 10;
}
```

### Overrides

Overrides can be used to customize components’ strings and localization-related
properties, such as locale and reading direction.

They can be set globally for all components using `LiveblocksUiConfig`:

```tsx
import { LiveblocksUiConfig } from "@liveblocks/react-ui";

export function App() {
  return (
    <LiveblocksUiConfig
      overrides={{ locale: "fr", USER_UNKNOWN: "Anonyme" /* ... */ }}
    >
      {/* ... */}
    </LiveblocksUiConfig>
  );
}
```

Overrides can also be set per-component, and these settings will take precedence
over global settings. This is particularly useful in certain contexts, for
example when you’re using a `<Composer />` component for creating replies to
threads:

```tsx
<Composer
  overrides={{
    COMPOSER_PLACEHOLDER: "Reply to thread…",
    COMPOSER_SEND: "Reply",
  }}
/>
```

#### Override names

Here's a list of all available override names, their descriptions, and default
values.

##### Localization

Localization overrides can only be set on
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name     | Description    | Default value |
| -------- | -------------- | ------------- |
| `locale` | Locale code    | `"en"`        |
| `dir`    | Text direction | `"ltr"`       |

##### Global

Global overrides can only be set on
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name                              | Description              | Default value                      |
| --------------------------------- | ------------------------ | ---------------------------------- |
| `USER_SELF`                       | Current user label       | `"you"`                            |
| `USER_UNKNOWN`                    | Unknown user label       | `"Anonymous"`                      |
| `LIST_REMAINING`                  | Remaining items          | `"X more"`                         |
| `LIST_REMAINING_USERS`            | Remaining users          | `"X others"`                       |
| `LIST_REMAINING_COMMENTS`         | Remaining comments       | `"X more comments"`                |
| `EMOJI_PICKER_SEARCH_PLACEHOLDER` | Emoji search placeholder | `"Search…"`                        |
| `EMOJI_PICKER_EMPTY`              | Empty emoji results      | `"No emoji found."`                |
| `EMOJI_PICKER_ERROR`              | Emoji picker error       | `"There was an error…"`            |
| `EMOJI_PICKER_CHANGE_SKIN_TONE`   | Skin tone button label   | `"Change skin tone"`               |
| `ATTACHMENT_TOO_LARGE`            | File too large error     | `"The file is larger than X"`      |
| `ATTACHMENT_ERROR`                | Upload error             | `"The file couldn't be uploaded."` |
| `COPY_TO_CLIPBOARD`               | Copy button label        | `"Copy"`                           |

##### Composer

Composer overrides can be set on both
[`Composer`](/docs/api-reference/liveblocks-react-ui#Composer) and
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name                         | Description             | Default value              |
| ---------------------------- | ----------------------- | -------------------------- |
| `COMPOSER_INSERT_MENTION`    | Mention button label    | `"Mention someone"`        |
| `COMPOSER_INSERT_EMOJI`      | Emoji button label      | `"Add emoji"`              |
| `COMPOSER_ATTACH_FILES`      | Attach button label     | `"Attach files"`           |
| `COMPOSER_REMOVE_ATTACHMENT` | Remove attachment label | `"Remove attachment"`      |
| `COMPOSER_PLACEHOLDER`       | Input placeholder       | `"Write a comment…"`       |
| `COMPOSER_SEND`              | Send button label       | `"Send"`                   |
| `COMPOSER_TOGGLE_MARK`       | Format toggle labels    | `"Bold"`, `"Italic"`, etc. |

##### Comment

Comment overrides can be set on both
[`Comment`](/docs/api-reference/liveblocks-react-ui#Comment) and
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name                                | Description             | Default value                      |
| ----------------------------------- | ----------------------- | ---------------------------------- |
| `COMMENT_EDITED`                    | Edited indicator        | `"(edited)"`                       |
| `COMMENT_DELETED`                   | Deleted message         | `"This comment has been deleted."` |
| `COMMENT_MORE`                      | More actions label      | `"More"`                           |
| `COMMENT_EDIT`                      | Edit action label       | `"Edit comment"`                   |
| `COMMENT_EDIT_COMPOSER_PLACEHOLDER` | Edit placeholder        | `"Edit comment…"`                  |
| `COMMENT_EDIT_COMPOSER_CANCEL`      | Cancel edit label       | `"Cancel"`                         |
| `COMMENT_EDIT_COMPOSER_SAVE`        | Save edit label         | `"Save"`                           |
| `COMMENT_DELETE`                    | Delete action label     | `"Delete comment"`                 |
| `COMMENT_DELETE_ATTACHMENT`         | Delete attachment label | `"Delete attachment"`              |
| `COMMENT_ADD_REACTION`              | Add reaction label      | `"Add reaction"`                   |
| `COMMENT_REACTION_LIST`             | Reaction list text      | `"X reacted with Y"`               |
| `COMMENT_REACTION_DESCRIPTION`      | Reaction description    | `"X reactions, react with Y"`      |

##### Thread

Thread overrides can be set on both
[`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) and
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name                               | Description               | Default value               |
| ---------------------------------- | ------------------------- | --------------------------- |
| `THREAD_RESOLVE`                   | Resolve action label      | `"Resolve thread"`          |
| `THREAD_UNRESOLVE`                 | Unresolve action label    | `"Re-open thread"`          |
| `THREAD_SUBSCRIBE`                 | Subscribe action label    | `"Subscribe to thread"`     |
| `THREAD_UNSUBSCRIBE`               | Unsubscribe action label  | `"Unsubscribe from thread"` |
| `THREAD_NEW_INDICATOR`             | New indicator label       | `"New"`                     |
| `THREAD_NEW_INDICATOR_DESCRIPTION` | New indicator description | `"New comments"`            |
| `THREAD_SHOW_MORE_COMMENTS`        | Show more label           | `"Show X more replies"`     |
| `THREAD_COMPOSER_PLACEHOLDER`      | Reply placeholder         | `"Reply to thread…"`        |
| `THREAD_COMPOSER_SEND`             | Reply button label        | `"Reply"`                   |

##### Inbox notification

Inbox notification overrides can be set on both
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification)
and
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name                                      | Description         | Default value            |
| ----------------------------------------- | ------------------- | ------------------------ |
| `INBOX_NOTIFICATION_MORE`                 | More actions label  | `"More"`                 |
| `INBOX_NOTIFICATION_MARK_AS_READ`         | Mark read label     | `"Mark as read"`         |
| `INBOX_NOTIFICATION_DELETE`               | Delete action label | `"Delete notification"`  |
| `INBOX_NOTIFICATION_THREAD_COMMENTS_LIST` | Comments list text  | `"X commented in Y"`     |
| `INBOX_NOTIFICATION_THREAD_MENTION`       | Thread mention text | `"X mentioned you in Y"` |
| `INBOX_NOTIFICATION_TEXT_MENTION`         | Text mention text   | `"X mentioned you in Y"` |

##### History version preview

History version preview overrides can be set on both
[`HistoryVersionSummary`](/docs/api-reference/liveblocks-react-ui#HistoryVersionSummary)
and
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name                                   | Description          | Default value           |
| -------------------------------------- | -------------------- | ----------------------- |
| `HISTORY_VERSION_PREVIEW_AUTHORS_LIST` | Authors list text    | `"Edits from X"`        |
| `HISTORY_VERSION_PREVIEW_RESTORE`      | Restore button label | `"Restore"`             |
| `HISTORY_VERSION_PREVIEW_EMPTY`        | Empty state text     | `"No content."`         |
| `HISTORY_VERSION_PREVIEW_ERROR`        | Error message        | `"There was an error…"` |

##### AI composer

AI composer overrides can be set on both
[`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) and
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name                      | Description        | Default value      |
| ------------------------- | ------------------ | ------------------ |
| `AI_COMPOSER_PLACEHOLDER` | Input placeholder  | `"Ask anything…"`  |
| `AI_COMPOSER_SEND`        | Send button label  | `"Send"`           |
| `AI_COMPOSER_ABORT`       | Abort button label | `"Abort response"` |

##### AI chat message

AI chat message overrides can be set on both
[`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) and
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name                        | Description         | Default value                       |
| --------------------------- | ------------------- | ----------------------------------- |
| `AI_CHAT_MESSAGE_DELETED`   | Deleted message     | `"This message has been deleted."`  |
| `AI_CHAT_MESSAGE_THINKING`  | Thinking indicator  | `"Thinking…"`                       |
| `AI_CHAT_MESSAGE_REASONING` | Reasoning indicator | `"Reasoning…"` / `"Reasoned for X"` |
| `AI_CHAT_MESSAGE_RETRIEVAL` | Retrieval indicator | `"Searching X…"` / `"Searched X"`   |

##### AI chat

AI chat overrides can be set on both
[`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) and
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name                     | Description    | Default value           |
| ------------------------ | -------------- | ----------------------- |
| `AI_CHAT_MESSAGES_ERROR` | Messages error | `"There was an error…"` |

##### AI tool confirmation

AI tool confirmation overrides can be set on both
[`AiTool`](/docs/api-reference/liveblocks-react-ui#AiTool) and
[`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig).

| Name                           | Description          | Default value |
| ------------------------------ | -------------------- | ------------- |
| `AI_TOOL_CONFIRMATION_CONFIRM` | Confirm button label | `"Confirm"`   |
| `AI_TOOL_CONFIRMATION_CANCEL`  | Cancel button label  | `"Cancel"`    |

---
meta:
  title: "@liveblocks/react"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/react package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/react` provides you with [React](https://react.dev/) bindings for
our realtime collaboration APIs, built on top of WebSockets. Read our
[getting started](/docs/get-started) guides to learn more.

## Suspense

All Liveblocks React components and hooks can be exported from two different
locations, `@liveblocks/react/suspense` and `@liveblocks/react`. This is because
Liveblocks provides two types of hooks; those that support
[React Suspense](https://react.dev/reference/react/Suspense), and those that
don’t.

```tsx
// Import the Suspense hook
import { useThreads } from "@/liveblocks/react/suspense";

// Import the regular hook
import { useThreads } from "@/liveblocks/react";
```

We recommend importing from `@liveblocks/react/suspense` and using Suspense by
default, as it often makes it easier to build your collaborative application.

<Banner title="Enable Suspense with createRoomContext">

If you’re using the non-standard [`createRoomContext`](#createRoomContext)
function to build your hooks, you must
[enable suspense differently](#createRoomContext-Suspense).

</Banner>

### Suspense hooks

Suspense hooks can be wrapped in [`ClientSideSuspense`][], which acts as a
loading spinner for any components below it. When using this, all components
below will only render once their hook contents have been loaded.

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

function App() {
  <ClientSideSuspense fallback={<div>Loading…</div>}>
    <Component />
  </ClientSideSuspense>;
}

function Component() {
  // `animals` is always defined
  const animals = useStorage((root) => root.animals);

  // ...
}
```

Advanced hooks using the `{ ..., error, isLoading }` syntax, such as
[`useThreads`][], can also use
[`ErrorBoundary`](https://github.com/bvaughn/react-error-boundary) to render an
error if the hook runs into a problem.

```tsx
import { ClientSideSuspense, useThreads } from "@liveblocks/react/suspense";
import { ErrorBoundary } from "react-error-boundary";

function App() {
  return (
    <ErrorBoundary fallback={<div>Error</div>}>
      <ClientSideSuspense fallback={<div>Loading…</div>}>
        <Component />
      </ClientSideSuspense>
    </ErrorBoundary>
  );
}

function Component() {
  // `threads` is always defined
  const { threads } = useThreads();

  // ...
}
```

An advantage of Suspense hooks is that you can have multiple different hooks in
your tree, and you only need a single `ClientSideSuspense` component to render a
loading spinner for all of them.

### Regular hooks

Regular hooks often return `null` whilst a component is loading, and you must
check for this to render a loading spinner.

```tsx
import { useStorage } from "@liveblocks/react";

function Component() {
  // `animals` is `null` when loading
  const animals = useStorage((root) => root.animals);

  if (!animals) {
    return <div>Loading…</div>;
  }

  // ...
}
```

Advanced hooks using the `{ ..., error, isLoading }` syntax, such as
[`useThreads`][], require you to make sure there isn’t a problem before using
the data.

```tsx
import { useThreads } from "@liveblocks/react";

function Component() {
  // Check for `error` and `isLoading` before `threads` is defined
  const { threads, error, isLoading } = useThreads();

  if (error) {
    return <div>Error</div>;
  }

  if (isLoading) {
    return <div>Loading…</div>;
  }

  // ...
}
```

### ClientSideSuspense

Liveblocks provides a component named `ClientSideSuspense` which works as a
replacement for `Suspense`. This is helpful as our Suspense hooks will throw an
error when they’re run on the server, and this component avoids this issue by
always rendering the `fallback` on the server.

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

function Page() {
  return (
    <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
      <RoomProvider id="my-room-id">
        +++
        <ClientSideSuspense fallback={<div>Loading…</div>}>
          <App />
        </ClientSideSuspense>
        +++
      </RoomProvider>
    </LiveblocksProvider>
  );
}
```

#### Loading spinners

Instead of wrapping your entire Liveblocks application inside a single
`ClientSideSuspense` component, you can use multiple of these components in
different parts of your application, and each will work as a loading fallback
for any components further down your tree.

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

function Page() {
  return (
    <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
      <RoomProvider id="my-room-id">
        <header>My title</header>

        <main>
          +++
          <ClientSideSuspense fallback={<div>Loading…</div>}>
            <Canvas />
          </ClientSideSuspense>
          +++
        </main>

        <aside>
          +++
          <ClientSideSuspense fallback={<div>Loading…</div>}>
            <LiveAvatars />
          </ClientSideSuspense>
          +++
        </aside>
      </RoomProvider>
    </LiveblocksProvider>
  );
}
```

This is a great way to build a static skeleton around your dynamic collaborative
application.

## Liveblocks

### LiveblocksProvider

Sets up a client for connecting to Liveblocks, and is the recommended way to do
this for React apps. You must define either `authEndpoint` or `publicApiKey`.
Resolver functions should be placed inside here, and a number of other options
are available, which correspond with those passed to [`createClient`][]. Unlike
[`RoomProvider`][], `LiveblocksProvider` doesn’t call Liveblocks servers when
mounted, and it should be placed higher in your app’s component tree.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
    // publicApiKey=""
    // authEndpoint="/api/liveblocks-auth"
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

```tsx title="All LiveblocksProvider props" isCollapsable isCollapsed
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      // Connect with authEndpoint
      authEndpoint="/api/liveblocks-auth"
      // ---
      // Alternatively, use an authEndpoint callback
      // authEndpoint={async (room) => {
      //   const response = await fetch("/api/liveblocks-auth", {
      //     method: "POST",
      //     headers: {
      //       Authentication: "<your own headers here>",
      //       "Content-Type": "application/json",
      //     },
      //     body: JSON.stringify({ room }),
      //   });
      //   return await response.json();
      // }}
      //
      // Alternatively, use a public key
      // publicApiKey="pk_..."
      //
      // Throttle time (ms) between WebSocket updates
      throttle={100}
      // ---
      // Prevent browser tab from closing while local changes aren’t synchronized yet
      preventUnsavedChanges={false}
      // ---
      // Throw lost-connection event after 5 seconds offline
      lostConnectionTimeout={5000}
      // ---
      // Disconnect users after X (ms) of inactivity, disabled by default
      backgroundKeepAliveTimeout={undefined}
      // ---
      // Resolve user info for Comments, Text Editor, and Notifications
      resolveUsers={async ({ userIds }) => {
        const usersData = await __getUsersFromDB__(userIds);

        return usersData.map((userData) => ({
          name: userData.name,
          avatar: userData.avatar.src,
        }));
      }}
      // ---
      // Resolve room info for Notifications
      resolveRoomsInfo={async ({ roomIds }) => {
        const documentsData = await __getDocumentsFromDB__(roomIds);

        return documentsData.map((documentData) => ({
          name: documentData.name,
          // url: documentData.url,
        }));
      }}
      // ---
      // Resolve group info for Comments and Text Editor
      resolveGroupsInfo={async ({ groupIds }) => {
        const groupsData = await __getGroupsFromDB__(groupIds);

        return groupsData.map((groupData) => ({
          avatar: groupData.avatar.src,
          name: groupData.name,
          // description: groupData.description,
        }));
      }}
      // ---
      // Resolve mention suggestions for Comments and Text Editor
      resolveMentionSuggestions={async ({ text, roomId }) => {
        const workspaceUsers = await __getWorkspaceUsersFromDB__(roomId);

        if (!text) {
          // Show all workspace users by default
          return __getUserIds__(workspaceUsers);
        } else {
          const matchingUsers = __findUsers__(workspaceUsers, text);
          return __getUserIds__(matchingUsers);
        }
      }}
      // ---
      // Polyfill options for non-browser environments
      polyfills={
        {
          // atob,
          // fetch,
          // WebSocket,
        }
      }
      // ---
      // Set the location of the "Powered by Liveblocks" badge
      // "top-right", "bottom-right", "bottom-left", "top-left"
      badgeLocation="bottom-right"
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

<PropertiesList title="Props">
  <PropertiesListItem
    name="authEndpoint"
    detailedType="string | async ((room?: string) => CustomAuthenticationResult)"
  >
    The URL of your back end’s [authentication endpoint](/docs/authentication)
    as a string, or an async callback function that returns a Liveblocks token
    result. Either `authEndpoint` or `publicApiKey` are required. Learn more
    about [using a URL string](#LiveblocksProviderAuthEndpoint) and [using a
    callback](#LiveblocksProviderCallback).
  </PropertiesListItem>
  <PropertiesListItem name="publicApiKey" type="string">
    The public API key taken from your project’s
    [dashboard](/dashboard/apikeys). Generally not recommended for production
    use. Either `authEndpoint` or `publicApiKey` are required. [Learn
    more](#LiveblocksProviderPublicKey).
  </PropertiesListItem>
  <PropertiesListItem name="throttle" type="number" defaultValue="100">
    The throttle time between WebSocket messages in milliseconds, a number
    between `16` and `1000` is allowed. Using `16` means your app will update 60
    times per second. [Learn more](#LiveblocksProviderThrottle).
  </PropertiesListItem>
  <PropertiesListItem
    name="preventUnsavedChanges"
    type="boolean"
    defaultValue="false"
  >
    When set, navigating away from the current page is prevented while
    Liveblocks is still synchronizing local changes. [Learn
    more](#prevent-users-losing-unsaved-changes).
  </PropertiesListItem>
  <PropertiesListItem
    name="lostConnectionTimeout"
    type="number"
    defaultValue="5000"
  >
    After a user disconnects, the time in milliseconds before a
    [`"lost-connection"`](/docs/api-reference/liveblocks-client#Room.subscribe.lost-connection)
    event is fired. [Learn more](#LiveblocksProviderLostConnectionTimeout).
  </PropertiesListItem>
  <PropertiesListItem name="backgroundKeepAliveTimeout" type="number">
    The time before an inactive WebSocket connection is disconnected. This is
    disabled by default, but setting a number will activate it. [Learn
    more](#LiveblocksProviderBackgroundKeepAliveTimeout).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveUsers"
    detailedType='async? (args: ResolveUsersArgs) => (UserMeta["info"] | undefined)[] | undefined'
  >
    A function that resolves user information in
    [Comments](/docs/ready-made-features/comments), [Text Editor](/docs/ready-made-features/text-editor), and [Notifications](/docs/ready-made-features/notifications). Return an array of
    `UserMeta["info"]` objects in the same order they arrived. [Learn
    more](#LiveblocksProviderResolveUsers).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveRoomsInfo"
    detailedType="async? (args: ResolveRoomsInfoArgs) => (RoomInfo | undefined)[] | undefined"
  >
    A function that resolves room information in
    [Notifications](/docs/ready-made-features/notifications). Return an array of
    `RoomInfo` objects in the same order they arrived. [Learn
    more](#LiveblocksProviderResolveRoomsInfo).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveGroupsInfo"
    detailedType="async? (args: ResolveGroupsInfoArgs) => (GroupInfo | undefined)[] | undefined"
  >
    A function that resolves group information in
    [Comments](/docs/ready-made-features/comments) and [Text Editor](/docs/ready-made-features/text-editor). Return an array of
    `GroupInfo` objects in the same order they arrived. [Learn
    more](#LiveblocksProviderResolveGroupsInfo).
  </PropertiesListItem>
  <PropertiesListItem
    name="resolveMentionSuggestions"
    detailedType="async? (args: ResolveMentionSuggestionsArgs) => string[] | MentionData[]"
  >
    A function that resolves mention suggestions in
    [Comments](/docs/ready-made-features/comments) and [Text Editor](/docs/ready-made-features/text-editor). Return an array of user IDs or mention objects.
    [Learn more](#LiveblocksProviderResolveMentionSuggestions).
  </PropertiesListItem>
  <PropertiesListItem name="polyfills">
    Place polyfills for `atob`, `fetch`, and `WebSocket` inside here. Useful
    when using a non-browser environment, such as
    [Node.js](#LiveblocksProviderNode) or [React
    Native](#LiveblocksProviderReactNative).
  </PropertiesListItem>
  <PropertiesListItem
    name="largeMessageStrategy"
    detailedType='"default" | "split" | "experimental-fallback-to-http"'
    defaultValue={`"default"`}
  >
    <div className="-mb-3">
      How to handle WebSocket messages that are larger than the maximum message size. Can be set to one of these values:
    </div>

    - `"default"` Don’t send anything, but log the error to the console and notify useErrorListener.
    - `"split"` Break the message up into chunks each of which is smaller than the maximum message size.
      Beware that using `"split"` will sacrifice atomicity of changes! Depending on your use case, this may or may not be problematic.
    - `"experimental-fallback-to-http"` Try sending the update over HTTP instead of WebSockets (experimental).

  </PropertiesListItem>
  <PropertiesListItem
    name="badgeLocation"
    detailedtype='"top-right" | "bottom-right" | "bottom-left" | "top-left"'
    defaultValue={`"bottom-right"`}
  >
    The location of the "Powered by Liveblocks" badge. Can be set to either `"top-right"`, `"bottom-right"`, `"bottom-left"`, or `"top-left"`.
    [Learn more](#Powered-by-Liveblocks-branding).
  </PropertiesListItem>
  <PropertiesListItem
    name="unstable_streamData"
    type="boolean"
    defaultValue="false"
  >
    Experimental. Stream the initial Storage content over HTTP, instead of
    waiting for a large initial WebSocket message to be sent from the server.
  </PropertiesListItem>
</PropertiesList>

#### LiveblocksProvider with public key [#LiveblocksProviderPublicKey]

When creating a client with a public key, you don’t need to set up an
authorization endpoint. We only recommend using a public key when prototyping,
or on public landing pages, as it makes it possible for end users to access any
room’s data. You should instead use an
[auth endpoint](#LiveblocksProviderAuthEndpoint).

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
      {/* children */}
    </LiveblocksProvider>
  );
}
```

#### LiveblocksProvider with auth endpoint [#LiveblocksProviderAuthEndpoint]

If you are not using a public key, you need to set up your own `authEndpoint`.
Please refer to our [Authentication guide](/docs/authentication).

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
      {/* children */}
    </LiveblocksProvider>
  );
}
```

#### LiveblocksProvider with auth endpoint callback [#LiveblocksProviderCallback]

If you need to add additional headers or use your own function to call your
endpoint, `authEndpoint` can be provided as a custom callback. You should return
the token created with
[`Liveblocks.prepareSession`](/docs/api-reference/liveblocks-node#access-tokens)
or [`liveblocks.identifyUser`](/docs/api-reference/liveblocks-node#id-tokens),
learn more in [authentication guide](/docs/rooms/authentication).

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      authEndpoint={async (room) => {
        // Fetch your authentication endpoint and retrieve your access or ID token
        // ...

        return { token: "..." };
      }}
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

`room` is the room ID that the user is connecting to. When using
[Notifications](/docs/ready-made-features/comments/email-notifications), `room`
can be `undefined`, as the client is requesting a token that grants access to
multiple rooms, rather than a specific room.

##### Fetch your endpoint

Here’s an example of fetching your API endpoint at `/api/liveblocks-auth` within
the callback.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      authEndpoint={async (room) => {
        const response = await fetch("/api/liveblocks-auth", {
          method: "POST",
          headers: {
            Authentication: "<your own headers here>",
            "Content-Type": "application/json",
          },
          // Don't forget to pass `room` down. Note that it
          // can be undefined when using Notifications.
          body: JSON.stringify({ room }),
        });
        return await response.json();
      }}
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

##### Token details

You should return the token created with
[`Liveblocks.prepareSession`](/docs/api-reference/liveblocks-node#access-tokens)
or [`liveblocks.identifyUser`](/docs/api-reference/liveblocks-node#id-tokens).
These are the values the functions can return.

1. A valid token, it returns a `{ "token": "..." }` shaped response.
1. A token that explicitly forbids access, it returns an
   `{ "error": "forbidden", "reason": "..." }` shaped response. If this is
   returned, the client will disconnect and won't keep trying to authorize.

Any other error will be treated as an unexpected error, after which the client
will retry the request until it receives either 1. or 2.

#### WebSocket throttle [#LiveblocksProviderThrottle]

By default, the client throttles the WebSocket messages sent to one every 100
milliseconds, which translates to 10 updates per second. It’s possible to
override that configuration with the `throttle` option with a value between `16`
and `1000` milliseconds.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      throttle={16}

      // Other options
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

This option is helpful for smoothing out realtime animations in your
application, as you can effectively increase the framerate without using any
interpolation. Here are some examples with their approximate frames per second
(FPS) values.

```ts
throttle:  16, // 60 FPS
throttle:  32, // 30 FPS
throttle: 200, //  5 FPS
```

#### Prevent users losing unsaved changes [#prevent-users-losing-unsaved-changes]

Liveblocks usually synchronizes milliseconds after a local change, but if a user
immediately closes their tab, or if they have a slow connection, it may take
longer for changes to synchronize. Enabling `preventUnsavedChanges` will stop
tabs with unsaved changes closing, by opening a dialog that warns users. In
usual circumstances, it will very rarely trigger.

```tsx
function Page() {
  return (
    <LiveblocksProvider
      // highlight-next-line
      preventUnsavedChanges

      // Other options
      // ...
    >
      ...
    </LiveblocksProvider>
  );
}
```

More specifically, this option triggers when:

- There are unsaved changes after calling any hooks or methods, in all of our
  products.
- There are unsaved changes in a
  [Text Editor](/docs/ready-made-features/text-editor).
- There’s an unsubmitted comment in the
  [Composer](/docs/api-reference/liveblocks-react-ui#Composer).
- The user has made changes and is currently offline.

Internally, this option uses the
[beforeunload event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event).

#### Lost connection timeout [#LiveblocksProviderLostConnectionTimeout]

If you’re connected to a room and briefly lose connection, Liveblocks will
reconnect automatically and quickly. However, if reconnecting takes longer than
usual, for example if your network is offline, then the room will emit an event
informing you about this.

How quickly this event is triggered can be configured with the
`lostConnectionTimeout` setting, and it takes a number in milliseconds.
`lostConnectionTimeout` can be set between `1000` and `30000` milliseconds. The
default is `5000`, or 5 seconds.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      lostConnectionTimeout={5000}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

You can listen to the event with [`useLostConnectionListener`][]. Note that this
also affects when `others` are reset to an empty array after a disconnection.
This helps prevent temporary flashes in your application as a user quickly
disconnects and reconnects. For a demonstration of this behavior, see our
[connection status example][].

#### Background keep-alive timeout [#LiveblocksProviderBackgroundKeepAliveTimeout]

By default, Liveblocks applications will maintain an active WebSocket connection
to the Liveblocks servers, even when running in a browser tab that’s in the
background. However, if you’d prefer for background tabs to disconnect after a
period of inactivity, then you can use `backgroundKeepAliveTimeout`.

When `backgroundKeepAliveTimeout` is specified, the client will automatically
disconnect applications that have been in an unfocused background tab for _at
least_ the specified time. When the browser tab is refocused, the client will
immediately reconnect to the room and synchronize the document.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      // Disconnect users after 15 minutes of inactivity
      backgroundKeepAliveTimeout={15 * 60 * 1000}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

`backgroundKeepAliveTimeout` accepts a number in milliseconds—we advise using a
value of at least a few minutes, to avoid unnecessary disconnections.

#### resolveUsers [#LiveblocksProviderResolveUsers]

[Comments](/docs/ready-made-features/comments) and
[Text Editor](/docs/ready-made-features/text-editor) store user IDs in its
system, but no other user information. To display user information in Comments,
Text Editor, and Notifications components, such as a user’s name or avatar, you
need to resolve these IDs into user objects. This function receives a list of
user IDs and you should return a list of user objects of the same size, in the
same order.

User IDs are automatically resolved in batches with a maximum of 50 users per
batch to optimize performance and prevent overwhelming your user resolution
function.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      resolveUsers={async ({ userIds }) => {
        const usersData = await __getUsersFromDB__(userIds);

        return usersData.map((userData) => ({
          name: userData.name,
          avatar: userData.avatar.src,
        }));
      }}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

The name and avatar you return are rendered in
[`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) components.

##### User objects

The user objects returned by the resolver function take the shape of
`UserMeta["info"]`, which contains `name` and `avatar` by default. These two
values are optional, though if you’re using the
[Comments default components](/docs/api-reference/liveblocks-react-ui#Components),
they are necessary. Here’s an example of `userIds` and the exact values
returned.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      resolveUsers={async ({ userIds }) => {
        // ["marc@example.com", "nimesh@example.com"];
        console.log(userIds);

        return [
          { name: "Marc", avatar: "https://example.com/marc.png" },
          { name: "Nimesh", avatar: "https://example.com/nimesh.png" },
        ];
      }}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

You can also return custom information, for example, a user’s `color`:

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      resolveUsers={async ({ userIds }) => {
        // ["marc@example.com"];
        console.log(userIds);

        return [
          {
            name: "Marc",
            avatar: "https://example.com/marc.png",
            // +++
            color: "purple",
            // +++
          },
        ];
      }}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

##### Accessing user data

You can access any values set within `resolveUsers` with the
[`useUser`](/docs/api-reference/liveblocks-react#useUser) hook.

```tsx
import { useUser } from "@liveblocks/react/suspense";

function Component() {
  const user = useUser("marc@example.com");

  // { name: "Marc", avatar: "https://...", ... }
  console.log(user);
}
```

#### resolveRoomsInfo [#LiveblocksProviderResolveRoomsInfo]

When using
[Notifications](/docs/ready-made-features/comments/email-notifications) with
[Comments](/docs/ready-made-features/comments), room IDs will be used to
contextualize notifications (e.g. “Chris mentioned you in _room-id_”) in the
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification)
component. To replace room IDs with more fitting names (e.g. document names,
“Chris mentioned you in _Document A_”), you can provide a resolver function to
the `resolveRoomsInfo` option in [`LiveblocksProvider`](#LiveblocksProvider).

This resolver function will receive a list of room IDs and should return a list
of room info objects of the same size and in the same order.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      resolveRoomsInfo={async ({ roomIds }) => {
        const documentsData = await __getDocumentsFromDB__(roomIds);

        return documentsData.map((documentData) => ({
          name: documentData.name,
          // url: documentData.url,
        }));
      }}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

In addition to the room’s name, you can also provide a room’s URL as the `url`
property. If you do so, the
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification)
component will automatically use it. It’s possible to use an inbox
notification’s `roomId` property to construct a room’s URL directly in React and
set it on
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification)
via `href`, but the room ID might not be enough for you to construct the URL,
you might need to call your backend for example. In that case, providing it via
`resolveRoomsInfo` is the preferred way.

#### resolveGroupsInfo [#LiveblocksProviderResolveGroupsInfo]

When using group mentions with [Comments](/docs/ready-made-features/comments)
and [Text Editor](/docs/ready-made-features/text-editor), group IDs will be used
instead of user IDs. Similarly to
[`resolveUsers`](#LiveblocksProviderResolveUsers), you can provide a resolver
function to the `resolveGroupsInfo` option in
[`LiveblocksProvider`](#LiveblocksProvider) to assign information like names and
avatars to group IDs.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      resolveGroupsInfo={async ({ groupIds }) => {
        const groupsData = await __getGroupsFromDB__(groupIds);

        return groupsData.map((groupData) => ({
          name: groupData.name,
          avatar: groupData.avatar.src,
          // description: groupData.description,
        }));
      }}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

##### Accessing group info

You can access any values set within `resolveGroupsInfo` with the
[`useGroupInfo`](/docs/api-reference/liveblocks-react#useGroupInfo) hook.

```tsx
import { useGroupInfo } from "@liveblocks/react/suspense";

function Component() {
  const group = useGroupInfo("group-engineering");

  // { name: "Engineering", avatar: "https://...", ... }
  console.log(group);
}
```

#### resolveMentionSuggestions [#LiveblocksProviderResolveMentionSuggestions]

To enable creating mentions in [Comments](/docs/ready-made-features/comments)
and [Text Editor](/docs/ready-made-features/text-editor), you can provide a
resolver function to the `resolveMentionSuggestions` option in
[`LiveblocksProvider`](#LiveblocksProvider). These mentions will be displayed in
the [`Composer`](/docs/api-reference/liveblocks-react-ui#Composer) component and
in text editors.

This resolver function will receive the mention currently being typed (e.g. when
writing “@jane”, `text` will be `jane`) and should return a list of user IDs
matching that text. This function will be called every time the text changes but
with some debouncing.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      resolveMentionSuggestions={async ({ text, roomId }) => {
        const workspaceUsers = await __getWorkspaceUsersFromDB__(roomId);

        if (!text) {
          // Show all workspace users by default
          return __getUserIds__(workspaceUsers);
        } else {
          const matchingUsers = __findUsers__(workspaceUsers, text);
          return __getUserIds__(matchingUsers);
        }
      }}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

##### Group mentions

To support group mentions in [Comments](/docs/ready-made-features/comments) and
[Text Editor](/docs/ready-made-features/text-editor), you can return a list of
mention objects instead of user IDs to suggest a mix of user and group mentions.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      resolveMentionSuggestions={async ({ text, roomId }) => {
        const dbUsers = await __findUsersFromDB__(roomId);
        const dbGroups = await __findGroupsFromDB__(roomId);

        // Show groups and users matching the text being typed
        return [
          ...dbGroups.map((group) => ({
            kind: "group",
            id: group.id,
          })),
          ...dbUsers.map((user) => ({
            kind: "user",
            id: user.id,
          })),
        ];
      }}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

The mention objects specify which kind of mention it is, the ID to mention (user
ID or group ID), etc.

```tsx
// A user mention suggestion
{
  kind: "user",
  id: "user-1",
}

// A group mention suggestion
{
  kind: "group",
  id: "group-1",
}

// A group mention suggestion with fixed group members
// When using fixed group members via `userIds`, they will take precedence
// if the group ID exists on Liveblocks.
{
  kind: "group",
  id: "here",
  userIds: ["user-1", "user-2"],
}
```

#### LiveblocksProvider for Node.js [#LiveblocksProviderNode]

To use `@liveblocks/client` in Node.js, you need to provide [`WebSocket`][] and
[`fetch`][] polyfills. As polyfills, we recommend installing [`ws`][] and
[`node-fetch`][].

```bash
npm install ws node-fetch
```

Then, pass them to the `LiveblocksProvider` polyfill option as below.

```tsx
import { LiveblocksProvider } from "@liveblocks/react/suspense";
import fetch from "node-fetch";
import WebSocket from "ws";

function App() {
  return (
    <LiveblocksProvider
      polyfills={{
        fetch,
        WebSocket,
      }}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

Note that `node-fetch` v3+
[does not support CommonJS](https://github.com/node-fetch/node-fetch/blob/main/docs/v3-UPGRADE-GUIDE.md#converted-to-es-module).
If you are using CommonJS, downgrade `node-fetch` to v2.

#### LiveblocksProvider for React Native [#LiveblocksProviderReactNative]

To use `@liveblocks/client` with [React Native](https://reactnative.dev/), you
need to add an [`atob`][] polyfill. As a polyfill, we recommend installing
[`base-64`][].

```bash
npm install base-64
```

Then you can pass the `decode` function to our `atob` polyfill option when you
create the client.

```ts
import { LiveblocksProvider } from "@liveblocks/react/suspense";
import { decode } from "base-64";

function App() {
  return (
    <LiveblocksProvider
      polyfills={{
        atob: decode,
      }}

      // Other props
      // ...
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

#### Powered by Liveblocks branding

By default, Liveblocks displays a "Powered by Liveblocks" badge in your
application. You can adjust the position of the badge by setting the
`badgeLocation` property on `LiveblocksProvider`.

```tsx title="Set badge location"
// "top-right", "bottom-right", "bottom-left", "top-left"
<LiveblocksProvider badgeLocation="bottom-right">
  <App />
</LiveblocksProvider>
```

If you wish to remove remove the badge entirely, you can do so by following
these steps:

1. In the Liveblocks dashboard, navigate to your
   [team’s settings](/dashboard/settings).
2. Under **General**, toggle on the remove "Powered by Liveblocks" branding
   option.

<Banner title='Removing the "Powered by Liveblocks" badge'>

Removing the "Powered by Liveblocks" badge on your projects requires a
[paid plan](/pricing/). See the [pricing page](/pricing/) for more information.

</Banner>

### createLiveblocksContext

<Banner title="Not recommended" type="warning">

This used to be the default way to start your app, but now it’s recommended for
advanced usage only. We generally recommend using [`LiveblocksProvider`][] and
following [typing your data with the Liveblocks interface](#Typing-your-data),
unless you need to define multiple room types in your application.

</Banner>

Creates a [`LiveblocksProvider`][] and a set of typed hooks. Note that any
`LiveblocksProvider` created in this way takes no props, because it uses
settings from the `client` instead. We recommend using it in
`liveblocks.config.ts` and re-exporting your typed hooks like below.

While [`createRoomContext`](#createRoomContext) offers APIs for interacting with
rooms (e.g. Presence, Storage, and Comments),
[`createLiveblocksContext`](#createLiveblocksContext) offers APIs for
interacting with Liveblocks features that are not tied to a specific room (e.g.
Notifications).

```tsx file="liveblocks.config.ts"
import { createClient } from "@liveblocks/client";
import { createRoomContext, createLiveblocksContext } from "@liveblocks/react";

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

// ...

export const { RoomProvider } = createRoomContext(client);

export const {
  LiveblocksProvider,
  useInboxNotifications,

  // Other hooks
  // ...
} = createLiveblocksContext(client);
```

### useClient [@badge=LiveblocksProvider]

Returns the [`client`](/docs/api-reference/liveblocks-client#createClient) of
the nearest [`LiveblocksProvider`][] above in the React component tree.

```ts
import { useClient } from "@liveblocks/react/suspense";

const client = useClient();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="client" type="Client">
    The [Liveblocks client](/docs/api-reference/liveblocks-client#createClient)
    instance from the nearest [`LiveblocksProvider`][].
  </PropertiesListItem>
</PropertiesList>

### useErrorListener [@badge=LiveblocksProvider]

Listen to potential Liveblocks errors. Examples of errors include room
connection errors, errors creating threads, and errors deleting notifications.
Each error has a `message` string, and a `context` object which has different
values for each error type. `context` always contains an error `type` and
`roomId`.

```ts
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  // { message: "You don't have access to this room", context: { ... }}
  console.error(error);
});
```

There are many different errors, and each can be handled separately by checking
the value of `error.context.type`. Below we’ve listed each error and the context
it provides.

```ts title="All error types" isCollapsable isCollapsed
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  switch (error.context.type) {
    // Can happen if you use Presence, Storage, or Yjs
    case "ROOM_CONNECTION_ERROR": {
      const { code } = error.context;
      // -1   = Authentication error
      // 4001 = You don't have access to this room
      // 4005 = Room was full
      // 4006 = Room ID has changed
      break;
    }

    // Can happen if you use Comments or Notifications
    case "CREATE_THREAD_ERROR":
      const { roomId, threadId, commentId, body, metadata } = error.context;
      break;

    case "DELETE_THREAD_ERROR":
      const { roomId, threadId } = error.context;
      break;

    case "EDIT_THREAD_METADATA_ERROR":
      const { roomId, threadId, metadata } = error.context;
      break;

    case "MARK_THREAD_AS_RESOLVED_ERROR":
    case "MARK_THREAD_AS_UNRESOLVED_ERROR":
      const { roomId, threadId } = error.context;
      break;

    case "CREATE_COMMENT_ERROR":
    case "EDIT_COMMENT_ERROR":
      const { roomId, threadId, commentId, body } = error.context;
      break;

    case "DELETE_COMMENT_ERROR":
      const { roomId, threadId, commentId } = error.context;
      break;

    case "ADD_REACTION_ERROR":
    case "REMOVE_REACTION_ERROR":
      const { roomId, threadId, commentId, emoji } = error.context;
      break;

    case "MARK_INBOX_NOTIFICATION_AS_READ_ERROR":
      const { inboxNotificationId, roomId } = error.context;
      break;

    case "DELETE_INBOX_NOTIFICATION_ERROR":
      const { roomId } = error.context;
      break;

    case "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR":
    case "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR":
      break;

    case "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR":
      const { roomId } = error.context;
      break;

    default:
      // Ignore any error from the future
      break;
  }
});
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="callback" type="(error: LiveblocksError) => void">
    A callback function that will be called when a Liveblocks error occurs. The
    error object contains a message and context with error-specific information.
  </PropertiesListItem>
</PropertiesList>

## AI Copilots

### useAiChats [@badge=LiveblocksProvider]

Returns a paginated list of AI chats created by the current user. Initially
fetches the latest 50 chats.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useAiChats } from "@liveblocks/react";

const { chats, error, isLoading } = useAiChats();
```

<PropertiesList title="Options">
  <PropertiesListItem name="query" type="AiChatsQuery">
    Optional query to filter chats by metadata values or absence of metadata
    keys. [Learn more](/docs/api-reference/liveblocks-react#useAiChats-query)
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="chats" type="AiChat[] | undefined">
    An array of AI chats created by the current user.
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the chats are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the chats. [Learn
    more](/docs/api-reference/liveblocks-react#useAiChats-error-handling).
  </PropertiesListItem>
  <PropertiesListItem name="hasFetchedAll" type="boolean">
    Whether all available chats have been fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useAiChats-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMore" type="() => void">
    A function to fetch more chats. [Learn
    more](/docs/api-reference/liveblocks-react#useAiChats-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="isFetchingMore" type="boolean">
    Whether more chats are currently being fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useAiChats-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMoreError" type="Error | undefined">
    Any error that occurred while fetching more chats. [Learn
    more](/docs/api-reference/liveblocks-react#useAiChats-pagination).
  </PropertiesListItem>
</PropertiesList>

##### List the user's chats and switch between them

You can use the [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat)
component alongside the hook to create an AI chat switcher. Below, each button
displays the chat's automatically generated title, and chats can be deleted with
[`useDeleteAiChat`](#useDeleteAiChat).

```tsx
import { useState } from "react";
import { AiChat } from "@liveblocks/react-ui";
import { useAiChats } from "@liveblocks/react";

function Chats() {
  // +++
  const { chats, error, isLoading } = useAiChats();
  const [chatId, setChatId] = useState();
  const deleteChat = useDeleteAiChat();
  // +++

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div style={{ display: "flex" }}>
      <ul>
        // +++
        {chats.map((chat) => (
          <li key={chat.id}>
            <button onClick={() => setChatId(chat.id)}>
              {chat.title || "Untitled"}
            </button>
            <button onClick={() => deleteChat(chat.id)}>❌</button>
          </li>
        ))}
        // +++
      </ul>
      // +++
      <AiChat chatId={chatId} />
      // +++
    </div>
  );
}
```

#### Querying chats [#useAiChats-query]

It’s possible to return chats that match a certain query with the `query`
option. You can filter by metadata values, or by the absence of a metadata key.
Returned chats must match the entire query.

```tsx
import { useAiChats } from "@liveblocks/react";

// Filter by metadata values and by absence of a key
const { chats } = useAiChats({
  query: {
    metadata: {
      // Match chats that are of type 'temporary'
      type: "temporary",

      // Match chats that have all of these tags
      tag: ["urgent", "billing"],

      // Match chats where the "archived" key does not exist
      archived: null,
    },
  },
});
```

#### Pagination [#useAiChats-pagination]

By default, the `useAiChats` hook returns up to 50 chats. To fetch more, the
hook provides additional fields for pagination, similar to [`useThreads`][].

```tsx
import { useAiChats } from "@liveblocks/react";

const {
  chats,
  isLoading,
  error,

  +++
  hasFetchedAll,
  fetchMore,
  isFetchingMore,
  fetchMoreError,
  +++
} = useAiChats();
```

- `hasFetchedAll` indicates whether all available AI chats have been fetched.
- `fetchMore` loads up to 50 more AI chats, and is always safe to call.
- `isFetchingMore` indicates whether more AI chats are being fetched.
- `fetchMoreError` returns error statuses resulting from fetching more.

##### Pagination example [#useAiChats-pagination-example]

The following example demonstrates how to use the `fetchMore` function to
implement a “Load More” button, which fetches additional AI chats when clicked.
The button is disabled while fetching is in progress.

```tsx
import { AiChat } from "@liveblocks/react-ui";
import { useAiChats } from "@liveblocks/react";

function Inbox() {
  const { chats, hasFetchedAll, fetchMore, isFetchingMore } = useAiChats();

  return (
    <div>
      {chats.map((chat) => (
        <AiChat key={chat.id} chatId={chat.id} />
      ))}
      // +++
      {hasFetchedAll ? (
        <div>🎉 All chats loaded!</div>
      ) : (
        <button disabled={isFetchingMore} onClick={fetchMore}>
          Load more
        </button>
      )}
      // +++
    </div>
  );
}
```

#### Error handling [#useAiChats-error-handling]

Error handling is another important aspect to consider when using the
`useAiChats` hook. The `error` and `fetchMoreError` fields provide information
about any errors that occurred during the initial fetch or subsequent fetch
operations, respectively. You can use these fields to display appropriate error
messages to the user and implement retry mechanisms if needed.

The following example shows how to display error messages for both initial
loading errors and errors that occur when fetching more inbox notifications.

```tsx
import { AiChat } from "@liveblocks/react-ui";
import { useAiChats } from "@liveblocks/react";

function Inbox() {
  const { chats, error, fetchMore, fetchMoreError } = useAiChats();

  // Handle error if the initial load failed.
  // The `error` field is not returned by the Suspense hook as the error is thrown to nearest ErrorBoundary
  // +++
  if (error) {
    return (
      <div>
        <p>Error loading AI chats: {error.message}</p>
      </div>
    );
  }
  // +++

  return (
    <div>
      {chats.map((chat) => (
        <AiChat key={chat.id} chatId={chat.id} />
      ))}

      {fetchMoreError && (
        <div>
          <p>Error loading more AI chats: {fetchMoreError.message}</p>
          <button onClick={fetchMore}>Retry</button>
        </div>
      )}
    </div>
  );
}
```

### useAiChat

Returns information about an AI chat, for example its title and metadata. Titles
are automatically generated from the content of the first user message in a
chat, and the AI’s response.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useAiChat } from "@liveblocks/react";

const { chat, error, isLoading } = useAiChat("my-chat-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="chatId" type="string">
    The ID of the AI chat to retrieve information for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="chat" type="AiChat | undefined">
    The AI chat object containing title, metadata, and other properties.
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the chat information is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the chat information.
  </PropertiesListItem>
</PropertiesList>

#### Displaying a default title

If `chat.title` is `undefined` after an `isLoading` check, that means the title
has not been set yet. You can display a default title in this case, and the
title will be displayed once generated.

```tsx
import { useAiChat } from "@liveblocks/react";

function ChatTitle() {
  const { chat, error, isLoading } = useAiChat("my-chat-id");

  if (isLoading || error) {
    return null;
  }

  // +++
  return <div>{chat.title || "Untitled chat"}</div>;
  // +++
}
```

### useCreateAiChat

Returns a function that creates an AI chat.

```tsx
import { useCreateAiChat } from "@liveblocks/react/suspense";

const createAiChat = useCreateAiChat();
createAiChat("my-ai-chat");
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="createAiChat"
    type="(chatIdOrOptions: string | CreateAiChatOptions) => AiChat"
  >
    A function that creates an AI chat. Can be called with either a string ID or
    an options object containing `id`, optional `title`, and optional
    `metadata`.
  </PropertiesListItem>
</PropertiesList>

#### Create a chat with a custom title and metadata

You can optionally set a `title` with `useCreateAiChat`, which prevents the AI
auto-generating a title from the first messages. Additionally, you can choose to
set custom `metadata` for the chat, strings or arrays of strings.

```tsx
import { useCreateAiChat } from "@liveblocks/react/suspense";

const createAiChat = useCreateAiChat();
createAiChat({
  id: "my-ai-chat",
  // +++
  title: "My AI Chat",
  metadata: {
    color: "red",
    tags: ["product", "engineering"],
  },
  // +++
});
```

### useDeleteAiChat

Returns a function that deletes an AI chat by its ID. Use in conjunction with
[`useAiChats`](#useAiChats) to
[loop through each chat](/docs/api-reference/liveblocks-react#List-the-user's-chats-and-switch-between-them)
and add a delete button.

```tsx
import { useDeleteAiChat } from "@liveblocks/react/suspense";

const deleteAiChat = useDeleteAiChat();
deleteAiChat("my-chat-id");
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="deleteAiChat" type="(chatId: string) => void">
    A function that deletes an AI chat by its ID.
  </PropertiesListItem>
</PropertiesList>

### useSendAiMessage

Returns a function that sends a message to an AI chat, identified by its ID.
Useful for
[creating suggestions in empty inside chats](/docs/api-reference/liveblocks-react-ui#AiChat-placeholder)
and sending messages on behalf of the user.

```tsx
import { useSendAiMessage } from "@liveblocks/react";

const sendAiMessage = useSendAiMessage("my-chat-id");
sendAiMessage("Hello!");
```

Remember to [set your copilot ID]() otherwise the default copilot will be used.

```tsx
const sendAiMessage = useSendAiMessage("my-chat-id", {
  // +++
  copilotId: "co_h7GBa3...",
  // +++
});
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="chatId" type="string">
    Optional. The ID of the AI chat to send messages to. Can also be provided
    when calling the returned function.
  </PropertiesListItem>
  <PropertiesListItem name="options" type="object">
    Optional configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="options.copilotId" type="string">
    Optional. The ID of the copilot to use for sending the message.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="sendAiMessage"
    type="(messageOrOptions: string | SendAiMessageOptions) => AiChatMessage"
  >
    A function that sends a message to an AI chat. Can be called with either a
    string message or an options object containing `text`, optional `chatId`,
    and optional `copilotId`.
  </PropertiesListItem>
</PropertiesList>

#### Setting options when sending a message

Optionally you can set options when sending a message, instead of when creating
the hook. Alternatively, you can also override or complete the hook’s options
when calling the function by passing an object to it.

```tsx
import { useSendAiMessage } from "@liveblocks/react";

// Setting a `chatId` and `copilotId`
const sendAiMessage = useSendAiMessage("my-chat-id", {
  copilotId: "co_shSm8f...",
});

// Sends to initial `chatId` and `copilotId`
sendAiMessage("Hello!");

// Overwrites the `copilotId` just for this message
sendAiMessage({
  text: "Hello world",
  // +++
  copilotId: "co_Xpksa9...",
  // +++
});

// Overwrites the `chatId` just for this message
sendAiMessage({
  text: "Hello world",
  // +++
  chatId: "my-other-chat-id",
  // +++
});
```

You can even skip setting the `chatId` and `copilotId` in the hook, and just
pass them in the function.

```tsx
// +++
const sendAiMessage = useSendAiMessage();
// +++

sendAiMessage({
  text: "Hello world",
  chatId: "my-other-chat-id",
});
```

#### Get the created message object

If necessary, you can also access the newly created message object.

```tsx
import { useSendAiMessage } from "@liveblocks/react";

const sendAiMessage = useSendAiMessage("my-chat-id");
const message = sendAiMessage("Hello world");

//  { id: "ms_gw1wEn...", chatId: "my-chat-id", content: [...], ...}
console.log(message);
```

### useAiChatMessages

Returns a list of every message in an AI chat, identified by its ID. Updates in
realtime using WebSockets.

```tsx
import { useAiChatMessages } from "@liveblocks/react";

const { messages, error, isLoading } = useAiChatMessages("my-chat-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="chatId" type="string">
    The ID of the AI chat to retrieve messages from.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="messages" type="AiChatMessage[] | undefined">
    An array of messages in the AI chat.

    <div className="-mt-1">
    ```tsx isCollapsable isCollapsed title="Example messages"
    [
      {
        "id": "ms_gw1wENvliU471QuojwVbO",
        "chatId": "my-chat-id",
        "parentId": null,
        "createdAt": "2025-09-30T15:37:01.000Z",
        "role": "user",
        "content": [
          {
            "type": "text",
            "text": "Hello"
          }
        ],
        "navigation": {
          "parent": null,
          "prev": null,
          "next": null
        }
      },
      {
        "id": "ms_dwoLgXARKgJT4zAjkktv6",
        "chatId": "my-chat-id",
        "parentId": "ms_gw1wENvliU471QuojwVbO",
        "createdAt": "2025-09-30T15:37:01.000Z",
        "copilotId": "co_lxYkxUdt08d01sJIBUZhg",
        "role": "assistant",
        "status": "completed",
        "content": [
          {
            "type": "text",
            "text": "Hi there! How can I help you today?\n",
            "t": 2783
          }
        ],
        "navigation": {
          "parent": "ms_gw1wENvliU471QuojwVbO",
          "prev": null,
          "next": null
        }
      }
    ]
    ```
    </div>

  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the messages are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the messages.
  </PropertiesListItem>
</PropertiesList>

### useAiChatStatus

Returns the status of an AI chat, indicating whether it’s disconnected, loading,
idle or actively generating content. This is a convenience hook that derives its
state from the latest assistant message in the chat.

```tsx
import { useAiChatStatus } from "@liveblocks/react";

const { status, partType, toolName } = useAiChatStatus("my-chat-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="chatId" type="string">
    The ID of the AI chat.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="status"
    type={`"disconnected" | "loading" | "idle" | "generating"`}
  >
    The current synchronization status of the chat. When disconnected the AI
    service is not available.
  </PropertiesListItem>
  <PropertiesListItem name="partType" type={`"text" | "tool-invocation"`}>
    The type of content being generated.
  </PropertiesListItem>
  <PropertiesListItem name="toolName" type="string | undefined">
    The name of the tool being invoked. If no tool is currently being called,
    returns `undefined`.
  </PropertiesListItem>
</PropertiesList>

### RegisterAiKnowledge

Adds knowledge to all AI features on the page. AI will understand the
information you pass, and will answer questions or call tools based on it. This
is particularly helpful for passing user info, app state, and other small
contextual knowledge.

```tsx
<RegisterAiKnowledge
  description="The current user's payment plan"
  value="Enterprise"
/>
```

Each knowledge source has a `description` string, and a `value` which can be
either a string, object, array or JSON-serializable value that provides
meaningful context for your use case. The AI uses this data, along with the
accompanying description, to better understand and respond to user queries or
perform actions based on the supplied context. These components can be placed
anywhere in your app, so long as they’re under
[`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider).

```tsx
import { AiChat } from "@liveblocks/react-ui";
import { RegisterAiKnowledge } from "@liveblocks/react";

function Chat() {
  return (
    <>
      // +++
      <RegisterAiKnowledge
        description="The current user's payment plan"
        value="Enterprise"
      />
      <RegisterAiKnowledge
        description="The current user's info"
        value={{
          name: "Jody Hekla",
          email: "jody@liveblocks.io",
          teams: ["Engineering", "Product"],
        }}
      />
      // +++
      <AiChat chatId="my-chat-id" />
    </>
  );
}
```

#### Pass in assorted context [#RegisterAiKnowledge-context]

Passing the AI context about the current datetime, the user’s language, the page
the user’s visiting, and navigable pages on your website, is an effective method
for improving your chat’s replies.

```tsx
import { RegisterAiKnowledge } from "@liveblocks/react";

function Chat() {
  return (
    <>
      // +++
      <RegisterAiKnowledge
        description="The current date and time"
        value={new Date()}
      />
      <RegisterAiKnowledge
        description="The user's preferred language"
        value={navigator.language}
      />
      <RegisterAiKnowledge
        description="The current URL"
        value={window.location.href}
      />
      <RegisterAiKnowledge
        description="URLs on this website"
        value={["/dashboard", "/billing", "/settings"]}
      />
      // +++
    </>
  );
}
```

When building your app, it’s worth considering which app-specific context will
be helpful, for example the user’s payment plan, or a list of their projects.

#### Pass in user data from your auth provider [#RegisterAiKnowledge-user]

You can pass in knowledge from your auth provider, for example with
[`useUser`](https://clerk.com/docs/hooks/use-user) from
[Clerk](https://clerk.com). You can tell AI that the state is loading in a
simple string, and it will understand.

```tsx
import { RegisterAiKnowledge } from "@liveblocks/react";
import { useUser } from "@clerk/clerk-react";

function Chat() {
  // +++
  const { isSignedIn, user, isLoaded } = useUser();
  // +++

  return (
    // +++
    <RegisterAiKnowledge
      description="The current user's info"
      value={isLoaded ? (isSignedIn ? user : "Not signed in") : "Loading..."}
    />
    // +++
  );
}
```

#### Pass in assorted data from fetching hooks [#RegisterAiKnowledge-fetching]

You can pass in knowledge from data fetching hooks such as with
[`useSWR`](https://swr.vercel.app/docs/getting-started) from
[SWR](https://swr.vercel.app/). You can tell AI that the state is loading in a
simple string, and it will understand.

```tsx
import { RegisterAiKnowledge } from "@liveblocks/react";
import useSWR from "swr";

function Chat() {
  // +++
  const { data, error, isLoading } = useSWR(`/important-data`, fetcher);
  // +++

  return (
    // +++
    <RegisterAiKnowledge
      description="Important data"
      value={isLoading ? "Loading..." : error ? "Problem fetching data" : data}
    />
    // +++
  );
}

const fetcher = (...args) => fetch(...args).then((res) => res.json());
```

#### Pass in text editor document data [#RegisterAiKnowledge-text-editor]

You can pass in knowledge from your text editor, for example when using
[Liveblocks Tiptap](/docs/api-reference/liveblocks-react-tiptap).

```tsx
import { RegisterAiKnowledge } from "@liveblocks/react-ui";
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension();

  // +++
  const editor = useEditor({
    extensions: [
      liveblocks,
      // ...
    ],
  });
  // +++

  return (
    <div>
      // +++
      <RegisterAiKnowledge
        description="The current document's Tiptap state"
        value={editor.getHTML()}
      />
      // +++
      <EditorContent editor={editor} />
    </div>
  );
}
```

As well as `editor.getHTML()`, `editor.getJSON()` and `editor.getText()` are
also available when using Tiptap. Its worth trying them all, in case your AI
model understands one of them better than the others.

{/* TODO how to make tools where you can write to the text editor */}

#### Pass in comment data [#RegisterAiKnowledge-storage]

You can also pass in knowledge from your custom Liveblocks Comments app with
[`useThreads`](/docs/api-reference/liveblocks-react#useThreads). This way, your
AI chat will understand the context in the current room.

```tsx
import { RegisterAiKnowledge, useThreads } from "@liveblocks/react";

function Comments() {
  // +++
  const { threads, isLoading, error } = useThreads();
  // +++

  return (
    <div>
      // +++
      <RegisterAiKnowledge
        description="The comment threads in the current document"
        value={
          isLoading
            ? "Loading..."
            : error
              ? "Problem fetching threads"
              : threads
        }
      />
      // +++
      {threads.map((thread) => (
        <Thread key={thread.id} thread={thread} />
      )}
    </div>
  );
}
```

#### Pass in storage data [#RegisterAiKnowledge-storage]

You can also pass in knowledge from your custom Liveblocks storage app with
[`useStorage`](/docs/api-reference/liveblocks-react#useStorage). This way, your
AI chat will understand the context in the current room.

```tsx
import { RegisterAiKnowledge, useStorage } from "@liveblocks/react/suspense";

function Whiteboard() {
  // +++
  const shapes = useStorage((root) => root.shapes);
  // +++

  return (
    <div>
      // +++
      <RegisterAiKnowledge
        description="The current shapes on the whiteboard"
        value={shapes}
      />
      // +++
      {shapes.map((shape) => (
        // ...
      )}
    </div>
  );
}
```

#### Props [#RegisterAiKnowledge-props]

<PropertiesList>
  <PropertiesListItem name="description" type="string" required>
    A clear description of what this knowledge represents. This helps the AI
    understand the context and relevance of the provided information.
  </PropertiesListItem>
  <PropertiesListItem name="value" type="Json" required>
    The actual data or information to share with the AI. Can be a string,
    object, array, or any JSON-serializable information that offers context
    relevant to your application or user.
  </PropertiesListItem>
  <PropertiesListItem name="id" type="string">
    Optional unique identifier for this knowledge source. If provided,
    subsequent updates with the same ID will replace the previous knowledge.
  </PropertiesListItem>
  <PropertiesListItem name="chatId" type="string">
    Optional chat ID to scope this tool to a specific chat. If provided, the
    tool will only be available to that chat.
  </PropertiesListItem>
</PropertiesList>

### RegisterAiTool

Registers a tool that can be used by
[AI chats](/docs/api-reference/liveblocks-react-ui#AiChat) on the page. Tools
allow AI to autonomously run actions, render custom components, and show
confirmation or human-in-the-loop UIs within the chat.

```tsx
import { RegisterAiTool } from "@liveblocks/react";
import { defineAiTool } from "@liveblocks/client";

<RegisterAiTool
  name="my-tool"
  tool={defineAiTool()({
    // Tool definition
    // ...
  })}
/>;
```

{/* TODO lots more info */}

[`defineAiTool`](/docs/api-reference/liveblocks-client#defineAiTool) is used to
create a tool definition, and you can supply `parameters` as a
[JSON Schema](https://json-schema.org/) that the AI can fill in. If you supply
an `execute` function the AI will call it. `render` is used to show UI inside
the chat. Below is an example of a tool that lets AI get the current weather in
a given location, then renders a component in the chat.

{/* TODO this all needs to be explained better */}

```tsx
function App() {
  return (
    <>
      // +++
      <RegisterAiTool
        name="get-weather"
        tool={defineAiTool()({
          description: "Get current weather information",
          parameters: {
            type: "object",
            properties: {
              location: { type: "string", description: "City name" },
            },
            required: ["location"],
            additionalProperties: false,
          },
          execute: async (args) => {
            const { temperature, condition } = await __getWeather__(
              args.location
            );
            return { data: { temperature, condition } };
          },
          render: ({ result }) => (
            <AiTool title="Weather Lookup" icon="🌤️">
              {result.data ? (
                <div>
                  {result.data.temperature}°F - {result.data.condition}
                </div>
              ) : null}
            </AiTool>
          ),
        })}
      />
      // +++
      <AiChat chatId="my-chat" />
    </>
  );
}
```

#### Tool that sends a toast notification

The following snippet shows a tool that lets AI send a toast notification with
[Sonner](https://sonner.emilkowal.ski/), then adds a message in the chat with
[`AiTool`](/docs/api-reference/liveblocks-react-ui#AiTool), letting the user
know that a toast was sent.

```tsx
import { AiChat } from "@liveblocks/react-ui";
import { RegisterAiTool } from "@liveblocks/react";
import { defineAiTool } from "@liveblocks/client";
import { toast, Toaster } from "sonner";

function Chat() {
  return (
    <>
      <RegisterAiTool
        name="send-toast-notification"
        tool={defineAiTool()({
          description: "Send a toast notification",

          parameters: {
            type: "object",
            properties: {
              message: {
                type: "string",
                description: "The message to display in the toast",
              },
            },
            required: ["message"],
            additionalProperties: false,
          },

          execute: async ({ message }) => {
            toast(message);
            return {
              data: { message },
              description: "You sent a toast",
            };
          },

          render: () => <AiTool title="Toast sent" icon="🍞" />,
        })}
      />
      <AiChat chatId="my-chat-id" />
      <Toaster />
    </>
  );
}
```

#### Scoping a tool to a specific chat

Tools can be scoped to specific chats by providing a `chatId` prop. When scoped,
the tool will only be available to that specific chat.

```tsx
import { AiChat } from "@liveblocks/react-ui";
import { RegisterAiTool } from "@liveblocks/react";
import { defineAiTool } from "@liveblocks/client";

function Chat() {
  return (
    <>
      <RegisterAiTool
        name="private-tool"
        tool={defineAiTool()({
          // Tool definition
          // ...
        })}
        // +++
        chatId="my-chat"
        // +++
      />
      // +++
      <AiChat chatId="my-chat" />
      // +++
    </>
  );
}
```

#### Props [#RegisterAiTool-props]

<PropertiesList>
  <PropertiesListItem name="name" type="string" required>
    Unique name for the tool. This is used internally to identify and manage the
    tool.
  </PropertiesListItem>
  <PropertiesListItem name="tool" type="AiOpaqueToolDefinition" required>
    The tool definition created with
    [`defineAiTool`](/docs/api-reference/liveblocks-client#defineAiTool).
  </PropertiesListItem>
  <PropertiesListItem name="chatId" type="string">
    Optional chat ID to scope this tool to a specific chat. If provided, the
    tool will only be available to that chat.
  </PropertiesListItem>
  <PropertiesListItem name="enabled" type="boolean">
    Whether this tool should be enabled. When set to `false`, the tool will not
    be made available to the AI copilot for any new/future chat messages, but
    will still allow existing tool invocations to be rendered that are part of
    the historic chat record. When provided as a prop to `RegisterAiTool`, it
    will take precedence over the value of the tool’s `enabled` value in
    `defineAiTool`.
  </PropertiesListItem>
</PropertiesList>

## Room

### RoomProvider

Makes a [`Room`][] available in the component hierarchy below. Joins the room
when the component is mounted, and automatically leaves the room when the
component is unmounted. When using
[Sync Datastore](/docs/platform/sync-datastore), initial Presence values for
each user, and Storage values for the room can be set.

```tsx
import { RoomProvider } from "@liveblocks/react/suspense";

function App() {
  return <RoomProvider id="my-room-id">{/* children */}</RoomProvider>;
}
```

<PropertiesList title="Props">
  <PropertiesListItem name="id" type="string" required>
    The unique ID for the current room. `RoomProvider` will join this room when
    it loads. If the room doesn’t exist already it will automatically create the
    room first then join. After setting up
    [authentication](/docs/authentication) for your app, it can helpful to
    decide on a naming pattern for your room IDs.
  </PropertiesListItem>
  <PropertiesListItem name="initialPresence" type="JsonObject">
    The initial Presence of the user entering the room. Each user has their own
    presence, and this is readable for all other connected users. A user’s
    Presence resets every time they disconnect. This object must be
    JSON-serializable. This value is ignored after the first render. [Learn
    more](#setting-initial-presence).
  </PropertiesListItem>
  <PropertiesListItem name="initialStorage" type="LsonObject">
    The initial Storage structure for the room when it’s joined for the first
    time. This is only set a single time, when the room has not yet been
    populated. This object must contain [conflict-free live
    structures](/docs/api-reference/liveblocks-client#Storage). This value is
    ignored after the first render, and if Storage for the current room has
    already been created. [Learn more](#setting-initial-storage).
  </PropertiesListItem>
  <PropertiesListItem name="autoConnect" type="boolean" defaultValue="true">
    Whether the room immediately connects to Liveblocks servers. This value is
    ignored after the first render.
  </PropertiesListItem>
</PropertiesList>

#### Setting initial Presence [#setting-initial-presence]

Presence is used for storing temporary user-based values, such as a user’s
cursor coordinates, or their current selection. Each user has their own
presence, and this is readable for all other connected users. Set your initial
Presence value by using `initialPresence`.

```tsx
import { RoomProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <RoomProvider
      id="my-room"
      // +++
      initialPresence={{
        cursor: null,
        colors: ["red", "purple"],
        selection: {
          id: 72426,
        },
      }}
      // +++
    >
      {/* children */}
    </RoomProvider>
  );
}
```

Each user’s Presence resets every time they disconnect, as this is only meant
for temporary data. Any JSON-serializable object is allowed (the `JsonObject`
type).

#### Setting initial Storage [#setting-initial-storage]

Storage is used to store permanent data that’s used in your application, such as
shapes on a whiteboard, nodes on a flowchart, or text in a form. The first time
a room is entered, you can set an initial value by using `initialStorage`.
`initialStorage` is only read and set a single time, unless a new top-level
property is added.

```tsx
import { LiveList, LiveObject, LiveMap } from "@liveblocks/client";
import { RoomProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <RoomProvider
      id="my-room"
      initialPresence={}
      // +++
      initialStorage={{
        title: "Untitled",
        names: new LiveList(["Steven", "Guillaume"]),
        shapes: new LiveMap([
          ["g9shu0", new LiveObject({ type: "rectangle", color: "red" })],
          ["djs3g5", new LiveObject({ type: "circle", color: "yellow" })],
        ]),
      }}
      // +++
    >
      {/* children */}
    </RoomProvider>
  );
}
```

If a new top-level property is added to `initialStorage`, the next time a user
connects, the new property will be created. Other properties will be unaffected.
Any
[conflict-free live structures](/docs/api-reference/liveblocks-client#Storage)
and JSON-serializable objects are allowed (the `LsonObject` type).

#### Speed up connecting to a room [#speed-up-connecting-to-a-room]

To speed up connecting to a room, you can call
[`Liveblocks.prewarmRoom`](/docs/api-reference/liveblocks-node#get-rooms-roomId-prewarm)
on the server, which will warm up a room for the next 10 seconds. Triggering
this directly before a user navigates to a room is an easy to way use this API.
Here’s a Next.js server actions example, showing how to trigger prewarming with
`onPointerDown`.

```ts title="actions.ts"
"use server";

import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function prewarmRoom(roomId: string) {
  // +++
  await liveblocks.prewarmRoom(roomId);
  // +++
}
```

```tsx title="RoomLink.tsx"
"use client";

import { prewarmRoom } from "../actions";
import Link from "next/link";

export function JoinButton({ roomId }: { roomId: string }) {
  return (
    // +++
    <Link href={`/rooms/${roomId}`} onPointerDown={() => prewarmRoom(roomId)}>
      // +++
      {roomId}
    </Link>
  );
}
```

`onPointerDown` is slightly quicker than `onClick` because it triggers before
the user releases their pointer.

### createRoomContext

<Banner>

This used to be the default way to start your app, but now it’s recommend for
advanced usage only. We generally recommend using [`LiveblocksProvider`][] and
following [typing your data with the Liveblocks interface](#Typing-your-data),
unless you need to define multiple room types in your application.

</Banner>

Creates a [`RoomProvider`][] and a set of typed hooks to use in your app. Note
that any `RoomProvider` created in this way does not need to be nested in
[`LiveblocksProvider`][], as it already has access to the `client`. We generally
recommend typing your app using the newer method instead. When using
`createRoomContext` it can be helpful to use it in `liveblocks.config.ts` and
re-export your typed hooks as below.

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

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

type Presence = {};
type Storage = {};
type UserMeta = {};
type RoomEvent = {};
type ThreadMetadata = {};
type CommentMetadata = {};

// +++
export const {
  RoomProvider,
  useMyPresence,

  // Other hooks
  // ...
} = createRoomContext<
  Presence,
  Storage,
  UserMeta,
  RoomEvent,
  ThreadMetadata,
  CommentMetadata
>(client);
// +++
```

#### Suspense with createRoomContext [#createRoomContext-Suspense]

To use the React suspense version of our hooks with `createRoomContext`, you can
export from the `suspense` property instead.

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

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

type Presence = {};
type Storage = {};
type UserMeta = {};
type RoomEvent = {};
type ThreadMetadata = {};
type CommentMetadata = {};

export const {
  // +++
  suspense: {
    RoomProvider,
    useMyPresence,

    // Other suspense hooks
    // ...
  },
  // +++
} = createRoomContext<
  Presence,
  Storage,
  UserMeta,
  RoomEvent,
  ThreadMetadata,
  CommentMetadata
>(client);
```

#### Typing createRoomContext

To type your hooks, you can pass multiple different types to
`createRoomContext`. A full explanation is in the code snippet below.

```tsx file="liveblocks.config.ts" isCollapsable isCollapsed
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";

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

// Presence represents the properties that exist on every user in the Room
// and that will automatically be kept in sync. Accessible through the
// `user.presence` property. Must be JSON-serializable.
type Presence = {
  // cursor: { x: number, y: number } | null,
  // ...
};

// Optionally, Storage represents the shared document that persists in the
// Room, even after all users leave. Fields under Storage typically are
// LiveList, LiveMap, LiveObject instances, for which updates are
// automatically persisted and synced to all connected clients.
type Storage = {
  // animals: LiveList<string>,
  // ...
};

// Optionally, UserMeta represents static/readonly metadata on each user, as
// provided by your own custom auth back end (if used). Useful for data that
// will not change during a session, like a user’s name or avatar.
// type UserMeta = {
//   id?: string,  // Accessible through `user.id`
//   info?: Json,  // Accessible through `user.info`
// };

// Optionally, the type of custom events broadcast and listened to in this
// room. Use a union for multiple events. Must be JSON-serializable.
// type RoomEvent = {};

// Optionally, when using Comments, ThreadMetadata represents metadata on
// each thread. Can only contain booleans, strings, and numbers.
// export type ThreadMetadata = {
//   pinned: boolean;
//   quote: string;
//   time: number;
// };

export const {
  RoomProvider,
  useMyPresence,
  useStorage,

  // Other hooks
  // ...
} = createRoomContext<
  Presence,
  Storage
  /* UserMeta, RoomEvent, ThreadMetadata */
>(client);
```

### useRoom [@badge=RoomProvider]

Returns the [`Room`][] of the nearest [`RoomProvider`][] above in the React
component tree.

```ts
import { useRoom } from "@liveblocks/react/suspense";

const room = useRoom();
```

Will throw when used outside of a [`RoomProvider`][]. If you don’t want this
hook to throw when used outside of a Room context (for example to write
components in a way that they can be used both inside and outside of a
Liveblocks room), you can use the `{ allowOutsideRoom }` option:

```ts
import { useRoom } from "@liveblocks/react/suspense";

const room = useRoom({ allowOutsideRoom: true }); // Possibly `null`
```

<PropertiesList title="Options">
  <PropertiesListItem
    name="allowOutsideRoom"
    type="boolean"
    defaultValue="false"
  >
    Whether the hook should return `null` instead of throwing when used outside
    of a [`RoomProvider`][] context.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="room" type="Room">
    The Room instance from the nearest [`RoomProvider`][]. Returns `null` if
    `allowOutsideRoom` is `true` and the hook is used outside of a room.
  </PropertiesListItem>
</PropertiesList>

### useIsInsideRoom [@badge=Both]

Returns a boolean, `true` if the hook was called inside a [`RoomProvider`][]
context, and `false` otherwise.

```ts
import { useIsInsideRoom } from "@liveblocks/react/suspense";

const isInsideRoom = useIsInsideRoom();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="isInsideRoom" type="boolean">
    `true` if the hook was called inside a [`RoomProvider`][] context, `false`
    otherwise.
  </PropertiesListItem>
</PropertiesList>

#### Displaying different components inside rooms

`useIsInsideRoom` is helpful for rendering different components depending on
whether they’re inside a room, or not. One example is a header component that
only displays a live avatar stack when users are connected to the room.

```tsx
import { useIsInsideRoom, useOthers } from "@liveblocks/react/suspense";

function Header() {
  // +++
  const isInsideRoom = useIsInsideRoom();
  // +++

  return (
    <div>
      // +++
      {isInsideRoom ? <LiveAvatars /> : null}
      // +++
      <MyAvatar />
    </div>
  );
}

function LiveAvatars() {
  const others = useOthers();
  return others.map((other) => <img src={other.info.picture} />);
}
```

Here’s how the example above would render in three different
[`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
and [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider)
contexts.

```tsx
// 👥👤 Live avatar stack and your avatar
<LiveblocksProvider /* ... */>
  <RoomProvider /* ... */>
    <Header />
  </RoomProvider>
</LiveblocksProvider>


// 👤 Just your avatar
<LiveblocksProvider /* ... */>
  <Header />
</LiveblocksProvider>

// 👤 Just your avatar
<Header />
```

### useStatus [@badge=RoomProvider]

Returns the current WebSocket connection status of the room, and will re-render
your component whenever it changes.

```ts
import { useStatus } from "@liveblocks/react/suspense";

const status = useStatus();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="status"
    type="'initial' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected'"
  >
    The current WebSocket connection status of the room.
  </PropertiesListItem>
</PropertiesList>

### useSyncStatus [@badge=Both]

Returns the current synchronization status of Liveblocks, and will re-render
your component whenever it changes. This includes any part of Liveblocks that
may be synchronizing local changes to the server, including (any room’s)
Storage, text editors, threads, or notifications.

A `{ smooth: true }` option is also available, which prevents quick changes
between states, making it ideal for
[rendering a synchronization badge in your app](#display-synchronization-badge).
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```ts
import { useSyncStatus } from "@liveblocks/react/suspense";

// "synchronizing" | "synchronized"
const syncStatus = useSyncStatus();
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="options" type="object">
    Optional configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="options.smooth" type="boolean" defaultValue="false">
    When `true`, prevents quick changes between states by delaying the
    transition from "synchronizing" to "synchronized" until 1 second has passed
    after the final change.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="syncStatus" type="'synchronizing' | 'synchronized'">
    The current synchronization status of Liveblocks.
  </PropertiesListItem>
</PropertiesList>

#### Display a synchronization badge [#display-synchronization-badge]

Passing `{ smooth: true }` prevents the status changing from `"synchronizing"`
to `"synchronized"` until 1 second has passed after the final change. This means
it’s ideal for rendering a synchronization status badge, as it won’t flicker in
a distracting manner when changes are made in quick succession.

```tsx
import { useSyncStatus } from "@liveblocks/react/suspense";

function StorageStatusBadge() {
  const syncStatus = useSyncStatus({ smooth: true });

  return <div>{syncStatus === "synchronized" ? "✅ Saved" : "🔄 Saving"}</div>;
}
```

#### Prevent users losing unsaved changes [#use-sync-status-prevent-users-losing-unsaved-changes]

Liveblocks usually synchronizes milliseconds after a local change, but if a user
immediately closes their tab, or if they have a slow connection, it may take
longer for changes to synchronize. Enabling `preventUnsavedChanges` will stop
tabs with unsaved changes closing, by opening a dialog that warns users. In
usual circumstances, it will very rarely trigger.

```tsx
function Page() {
  return (
    <LiveblocksProvider
      // highlight-next-line
      preventUnsavedChanges

      // Other options
      // ...
    >
      ...
    </LiveblocksProvider>
  );
}
```

More specifically, this option triggers when:

- There are unsaved changes after calling any hooks or methods, in all of our
  products.
- There are unsaved changes in a
  [Text Editor](/docs/ready-made-features/text-editor).
- There’s an unsubmitted comment in the
  [Composer](/docs/api-reference/liveblocks-react-ui#Composer).
- The user has made changes and is currently offline.

Internally, this option uses the
[beforeunload event](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event).

### useOthersListener [@badge=RoomProvider]

Calls the given callback when an “others” event occurs, when a user enters,
leaves, or updates their presence.

```ts
function App() {
  useOthersListener(({ type, user, others }) => {
    switch (type) {
      case "enter":
        // `user` has entered the room
        break;

      case "leave":
        // `user` has left the room
        break;

      case "update":
        // Presence for `user` has updated
        break;

      case "reset":
        // Others list has been emptied
        break;
    }
  });
}
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="callback" type="(event: OthersEvent) => void">
    A callback function that is called when an "others" event occurs. The event
    object contains the error `type`,  the `user` that triggered it, and the current
    `others` in the room. Possible event types are:

      - `enter` – A user has entered the room.
      - `leave` – A user has left the room.
      - `reset` – The others list has been emptied. This is the first event that
        occurs when the room is entered. It also occurs when you’ve lost connection to
        the room.
      - `update` – A user’s presence data has been updated.

    </PropertiesListItem>

</PropertiesList>

### useLostConnectionListener [@badge=RoomProvider]

Calls the given callback in the exceptional situation that a connection is lost
and reconnecting does not happen quickly enough.

This event allows you to build high-quality UIs by warning your users that the
app is still trying to re-establish the connection, for example through a toast
notification. You may want to take extra care in the mean time to ensure their
changes won’t go unsaved.

When this happens, this callback is called with the event `lost`. Then, once the
connection restores, the callback will be called with the value `restored`. If
the connection could definitively not be restored, it will be called with
`failed` (uncommon).

The [`lostConnectionTimeout`][] client option will determine how quickly this
event will fire after a connection loss (default: 5 seconds).

```ts
import { toast } from "my-preferred-toast-library";

function App() {
  useLostConnectionListener((event) => {
    switch (event) {
      case "lost":
        toast.warn("Still trying to reconnect...");
        break;

      case "restored":
        toast.success("Successfully reconnected again!");
        break;

      case "failed":
        toast.error("Could not restore the connection");
        break;
    }
  });
}
```

Automatically unsubscribes when the component is unmounted. For a demonstration
of this behavior, see our [connection status example][].

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    type="(event: 'lost' | 'restored' | 'failed') => void"
  >
    A callback function that is called when a connection loss event occurs. The
    event can be "lost", "restored", or "failed".
  </PropertiesListItem>
</PropertiesList>

## Presence

<Banner title="Need help troubleshooting presence?">

Try the [Liveblocks DevTools extension](/devtools) to visualize your
collaborative experiences as you build them, in realtime.

</Banner>

### useMyPresence [@badge=RoomProvider]

Return the presence of the current user, and a function to update it.
Automatically subscribes to updates to the current user’s presence. Setting a
property will not replace the whole state, but will instead merge the property
into the existing state.

```ts
import { useMyPresence } from "@liveblocks/react/suspense";

const [myPresence, updateMyPresence] = useMyPresence();

updateMyPresence({ x: 0 });
updateMyPresence({ y: 0 });

// At the next render, "myPresence" will be equal to "{ x: 0, y: 0 }"
```

<PropertiesList title="Returns">
  <PropertiesListItem name="myPresence" type="TPresence">
    The current user’s presence data.
  </PropertiesListItem>
  <PropertiesListItem
    name="updateMyPresence"
    type="(patch: Partial<Presence>, options?: { addToHistory?: boolean }) => void"
  >
    A function to update the current user’s presence. Accepts a partial presence
    object and optional history options.
  </PropertiesListItem>
</PropertiesList>

#### Adding presence to history

`updateMyPresence` accepts an optional argument to add a new item to the
undo/redo stack. See [`room.history`][] for more information.

```ts
updateMyPresence({ selectedId: "xxx" }, { addToHistory: true });
```

#### Other ways to use presence

`useMyPresence` is a more convenient way to update and view presence, rather
than using [`useSelf`][] and [`useUpdateMyPresence`][] in combination.

```tsx
const myPresence = useSelf((me) => me.presence);
const updateMyPresence = useUpdateMyPresence();
```

### useUpdateMyPresence [@badge=RoomProvider]

Returns a setter function to update the current user’s presence. Setting a
property will not replace the whole state, but will instead merge the property
into the existing state. Will trigger fewer renders than [`useMyPresence`][], as
it doesn’t update when presence changes.

```ts
import { useUpdateMyPresence } from "@liveblocks/react/suspense";

const updateMyPresence = useUpdateMyPresence();

updateMyPresence({ y: 0 });
updateMyPresence({ x: 0 });

// Presence will be { x: 0, y: 0 }
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="updateMyPresence"
    type="(patch: Partial<Presence>, options?: { addToHistory?: boolean }) => void"
  >
    A function to update the current user's presence. Accepts a partial presence
    object and optional history options.
  </PropertiesListItem>
</PropertiesList>

#### Adding presence to history

`updateMyPresence` accepts an optional argument to add a new item to the
undo/redo stack. See [`room.history`][] for more information.

```ts
updateMyPresence({ selectedId: "xxx" }, { addToHistory: true });
```

### useSelf [@badge=RoomProvider]

Returns the current user once it is connected to the room, and automatically
subscribes to updates to the current user.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```ts
import { useSelf } from "@liveblocks/react/suspense";

const currentUser = useSelf();
// {
//   connectionId: 1,
//   presence: { cursor: { x: 27, y: -8 } },
// }

const currentUser = useSelf((me) => me.presence.cursor);
// { x: 27, y: -8 }
```

The benefit of using a selector is that it will only update your component if
that particular selection changes. For full details, see [how selectors work][].

<PropertiesList title="Arguments">
  <PropertiesListItem name="selector" type="(me: User) => T">
    Optional selector function to extract specific data from the current user.
    If not provided, returns the entire user object.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="currentUser" type="User | T | null">
    The current user object or the selected data from the user. Returns `null`
    if not connected to the room (in non-Suspense version).
  </PropertiesListItem>
</PropertiesList>

#### Checking user permissions

It’s possible to check if a user has a specific permission by using the
`canWrite` and `canComment` properties of the `User` object. This is set via
your [room permissions](/docs/authentication#Room-permissions).

```ts
import { useSelf } from "@liveblocks/react/suspense";

function PermissionBadge() {
  const canWrite = useSelf((me) => me.canWrite);

  if (canWrite) {
    return <div>✏️ Full access</div>;
  }

  return <div>👀 Read-only access</div>;
}
```

This is particularly helpful in combination with text editors, such as
[Tiptap](/docs/api-reference/liveblocks-react-tiptap) as you can prevent
read-only users from editing the document.

```tsx
import { useSelf } from "@liveblocks/react/suspense";
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
import { useEditor, EditorContent } from "@tiptap/react";

function TextEditor() {
  const liveblocks = useLiveblocksExtension();
  // +++
  const canWrite = useSelf((me) => me.canWrite);
  // +++

  const editor = useEditor({
    // +++
    editable: canWrite,
    // +++
    extensions: [
      liveblocks,
      // ...
    ],
  });

  return (
    <div>
      <EditorContent editor={editor} />
    </div>
  );
}
```

### useOthers [@badge=RoomProvider]

Extracts data from the list of other users currently in the same Room, and
automatically subscribes to updates on the selected data. For full details, see
[how selectors work][].
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

The `others` argument to the `useOthers` selector function is an _immutable_
array of Users.

```tsx
// ✅ Rerenders only if the number of users changes
const numOthers = useOthers((others) => others.length);

// ✅ Rerenders only if someone starts or stops typing
const isSomeoneTyping = useOthers((others) =>
  others.some((other) => other.presence.isTyping)
);

// ✅ Rerenders only if actively typing users are updated
const typingUsers = useOthers(
  (others) => others.filter((other) => other.presence.isTyping),
  shallow // 👈
);
```

One caveat with this API is that selecting a subset of data for each user
quickly becomes tricky. When you want to select and get updates for only a
particular subset of each user’s data, we recommend using the
[`useOthersMapped`][] hook instead, which is optimized for this use case.

```tsx
// ❌ Mapping is hard to get right with this hook
const cursors = useOthers(
  (others) => others.map((other) => other.presence.cursor),
  shallow
);

// ✅ Better to use useOthersMapped
const cursors = useOthersMapped((other) => other.presence.cursor);
```

When called without arguments, returns the user list and updates your component
whenever _anything_ in it changes. This might be way more often than you want!

```tsx
const others = useOthers(); // ⚠️ Caution, might rerender often!
// [
//   { connectionId: 2, presence: { cursor: { x: 27, y: -8 } } },
//   { connectionId: 3, presence: { cursor: { x: 0, y: 19 } } },
// ]
```

<Banner type="error" title="Caution">

In production-ready apps, you likely want to avoid calling `useOthers` without
arguments.

</Banner>

<PropertiesList title="Arguments">
  <PropertiesListItem name="selector" type="(others: readonly User[]) => T">
    Optional selector function to extract specific data from the others array.
    If not provided, returns the entire others array.
  </PropertiesListItem>
  <PropertiesListItem name="isEqual" type="(prev: T, curr: T) => boolean">
    Optional equality function to determine if the selected data has changed.
    Defaults to strict equality comparison.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="others" type="readonly TUser[] | null">
    The others array or the selected data from the others array. Returns `null`
    if not connected to the room (in non-Suspense version).
  </PropertiesListItem>
</PropertiesList>

### useOthersMapped [@badge=RoomProvider]

Extract data using a [selector][] for every user in the room, and subscribe to
all changes to the selected data. A [Suspense version][] of this hook is also
available. The key difference with [`useOthers`][] is that the selector (and the
optional comparison function) work at the _item_ level, like doing a `.map()`
over the others array.

```tsx
// Example 1
const others = useOthersMapped((other) => other.presence.cursor);
// [
//   [2, { x: 27, y: -8 }],
//   [3, { x: 0, y: 19 }],
// ]

// Example 2
const others = useOthersMapped(
  (other) => ({
    avatar: other.info.avatar,
    isTyping: other.presence.isTyping,
  }),
  shallow // 👈
);

// [
//   [2, { avatar: 'https://...', isTyping: true }],
//   [3, { avatar: null, isTyping: false }],
// ]
```

Returns an array where each item is a pair of `[connectionId, data]`. For
pragmatic reasons, the results are keyed by the `connectionId`, because in most
cases you’ll want to iterate over the results and draw some UI for each, which
in React requires you to use a `key={connectionId}` prop.

```tsx
const others = useOthersMapped((other) => other.presence.cursor);

// In JSX
return (
  <>
    {others.map(([connectionId, cursor]) => (
      <Cursor key={connectionId} x={cursor.x} y={cursor.y} />
    ))}
  </>
);
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="selector" type="(other: User) => T">
    A selector function to extract specific data from each user in the others
    array.
  </PropertiesListItem>
  <PropertiesListItem name="isEqual" type="(prev: T, curr: T) => boolean">
    Optional equality function to determine if the selected data for a user has
    changed. Defaults to strict equality comparison.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="others"
    type="readonly [connectionId: number, data: T][] | null"
  >
    An array of tuples where each item is a pair of `[connectionId,
    selectedData]`. Returns `null` if not connected to the room (in non-Suspense
    version).
  </PropertiesListItem>
</PropertiesList>

### useOthersConnectionIds [@badge=RoomProvider]

Returns an array of connection IDs (numbers), and rerenders automatically when
users join or leave. This hook is useful in particular in combination with the
[`useOther`][] (singular) hook, to implement high-frequency rerendering of
components for each user in the room, e.g. cursors. See the [`useOther`][]
(singular) documentation below for a full usage example.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useOthersConnectionIds } from "@liveblocks/react/suspense";

// [2, 4, 7]
const connectionIds = useOthersConnectionIds();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="connectionIds" type="readonly number[] | null">
    An array of connection IDs for all other users in the room. Returns `null`
    if not connected to the room (in non-Suspense version).
  </PropertiesListItem>
</PropertiesList>

#### Another way to fetch connection IDs

This hook is similar to using [`useOthers`][] and calling `.map()` on the
result.

```tsx
import { useOthers, shallow } from "@liveblocks/react/suspense";

// [2, 4, 7]
const connectionIds = useOthers(
  (others) => others.map((other) => other.connectionId),
  shallow
);
```

### useOther [@badge=RoomProvider]

Extract data using a [selector][] for one specific user in the room, and
subscribe to all changes to the selected data.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useOther } from "@liveblocks/react/suspense";

// ✅ Rerenders when this specific user’s isTyping changes (but not when their cursor changes)
const isTyping = useOther(
  3, // User with connectionId 3
  (user) => user.presence.isTyping
);
```

The reason this hook exists is to enable the most efficient rerendering model
for high-frequency updates to other’s presences, which is the following
structure:

```tsx file="Cursors.tsx"
const Cursors =
  // +++
  // (1) Wrap parent component in a memo and make sure it takes no props
  React.memo(function () {
    const othersConnectionIds = useOthersConnectionIds(); // (2)
    // +++
    return (
      <>
        {othersConnectionIds.map((connectionId) => (
          <Cursor
            // +++
            key={connectionId} // (3)
            // +++
            connectionId={connectionId}
          />
        ))}
      </>
    );
  });
```

```tsx file="Cursor.tsx"
function Cursor({ connectionId }) {
  // +++
  const { x, y } = useOther(connectionId, (other) => other.presence.cursor); // (4)
  // +++
  return <Cursor x={x} y={y} />;
}
```

1. Makes sure this whole component tree will never rerender beyond the first
   time.
2. Makes sure the parent component only rerenders when users join/leave.
3. Makes sure each cursor remains associated to the same connection.
4. Makes sure each cursor rerenders whenever _its_ data changes only.

👉 A [Suspense version][] of this hook is also available, which will never
return `null`.

<PropertiesList title="Arguments">
  <PropertiesListItem name="connectionId" type="number">
    The connection ID of the specific user to extract data from.
  </PropertiesListItem>
  <PropertiesListItem name="selector" type="(other: User) => T">
    A selector function to extract specific data from the user.
  </PropertiesListItem>
  <PropertiesListItem name="isEqual" type="(prev: T, curr: T) => boolean">
    Optional equality function to determine if the selected data has changed.
    Defaults to strict equality comparison.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="data" type="T | null">
    The selected data from the specified user. Returns `null` if the user is not
    found or not connected to the room (in non-Suspense version).
  </PropertiesListItem>
</PropertiesList>

## Broadcast

### useBroadcastEvent [@badge=RoomProvider]

Returns a callback that lets you broadcast custom events to other users in the
room.

```ts
import { useBroadcastEvent } from "@liveblocks/react/suspense";

// +++
// On client A
const broadcast = useBroadcastEvent();
broadcast({ type: "EMOJI", emoji: "🔥" });
// +++

// On client B
useEventListener(({ event, user, connectionId }) => {
  //                       ^^^^ Will be Client A
  if (event.type === "EMOJI") {
    // Do something
  }
});
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="broadcast" type="(event: TBroadcastEvent) => void">
    A function that broadcasts custom events to other users in the room.
  </PropertiesListItem>
</PropertiesList>

### useEventListener [@badge=RoomProvider]

Listen to custom events sent by other people in the room via
[`useBroadcastEvent`][]. Provides the `event` along with the `connectionId` of
the user that sent the message. If an event was sent from the
[Broadcast to a room](/docs/api-reference/rest-api-endpoints#post-broadcast-event)
REST API, `connectionId` will be `-1`.

```ts
import { useEventListener } from "@liveblocks/react/suspense";

// On client A
const broadcast = useBroadcastEvent();
broadcast({ type: "EMOJI", emoji: "🔥" });

// +++
// On client B
useEventListener(({ event, user, connectionId }) => {
  //                       ^^^^ Will be Client A
  if (event.type === "EMOJI") {
    // Do something
  }
});
// +++
```

The `user` property will indicate which User instance sent the message. This
will typically be equal to one of the others in the room, but it can also be
`null` in case this event was broadcasted from the server, using the
[Broadcast Event API](https://liveblocks.io/docs/api-reference/rest-api-endpoints#post-broadcast-event).

Automatically unsubscribes when the component is unmounted.

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="callback"
    type="(event: { event: TBroadcastEvent; user: User | null; connectionId: number }) => void"
  >
    A callback function that is called when a custom event is received. The
    callback receives an object with the event data, user information, and
    connection ID. Connection ID is always `-1` when receiving an event sent
    from the server.
  </PropertiesListItem>
</PropertiesList>

## Storage

Each room contains Storage, a conflict-free data store that multiple users can
edit at the same time. When users make edits simultaneously, conflicts are
resolved automatically, and each user will see the same state. Storage is ideal
for storing permanent document state, such as shapes on a canvas, notes on a
whiteboard, or cells in a spreadsheet.

### Data structures

Storage provides three different conflict-free data structures, which you can
use to build your application. All structures are permanent and persist when all
users have left the room, unlike [Presence](/docs/ready-made-features/presence)
which is temporary.

- [`LiveObject`][] - Similar to JavaScript object. Use this for storing records
  with fixed key names and where the values don’t necessarily have the same
  types. For example, a `Person` with a `name: string` and an `age: number`
  field. If multiple clients update the same property simultaneously, the last
  modification received by the Liveblocks servers is the winner.

- [`LiveList`][] - An ordered collection of items synchronized across clients.
  Even if multiple users add/remove/move elements simultaneously, LiveList will
  solve the conflicts to ensure everyone sees the same collection of items.

- [`LiveMap`][] - Similar to a JavaScript Map. Use this for indexing values that
  all have the same structure. For example, to store an index of `Person` values
  by their name. If multiple users update the same property simultaneously, the
  last modification received by the Liveblocks servers is the winner.

### Typing Storage [#typing-storage]

To type the Storage values you receive, make sure to set your `Storage` type.

```ts
import { LiveList } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    Storage: {
      animals: LiveList<string>;
    };
  }
}
```

You can then set an initial value in [`RoomProvider`][].

```tsx
import { LiveList } from "@liveblocks/client";
import { RoomProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <RoomProvider
      id="my-room-name"
      // +++
      initialStorage={{ animals: new LiveList(["Fido"]) }}
      // +++
    >
      {/* children */}
    </RoomProvider>
  );
}
```

The type received in your Storage will match the type passed. Learn more under
[typing your data](#typing-your-data).

```tsx
import { useMutation } from "@liveblocks/react/suspense";

function App() {
  const addAnimal = useMutation(({ storage }) => {
    const animals = storage.get("animals");

    // LiveList<["Fido"]>
    console.log(animals);

    animals.push("Felix");

    // LiveList<["Fido", "Felix"]>
    console.log(animals);
  });

  return <button onClick={addAnimal}>Add animal</button>;
}
```

[`useStorage`][] will return an immutable copy of the data, for example a
`LiveList` is converted to an `array`, which makes it easy to render.

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

function App() {
  const animals = useStorage((root) => root.animals);

  // ["Fido", "Felix"]
  console.log(animals);

  return (
    <ul>
      {animals.map((animal) => (
        <li key={animal}>{animal}</li>
      ))}
    </ul>
  );
}
```

### Nesting data structures

All Storage data structures can be nested, allowing you to create complex trees
of conflict-free data.

```ts
import { LiveObject, LiveList, LiveMap } from "@liveblocks/client";

type Person = LiveObject<{
  name: string;
  pets: LiveList<string>;
}>;

declare global {
  interface Liveblocks {
    Storage: {
      people: LiveMap<string, Person>;
    };
  }
}
```

Here’s an example of setting `initialStorage` for this type.

```tsx
import { LiveObject, LiveList, LiveMap } from "@liveblocks/client";
import { RoomProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <RoomProvider
      id="my-room-name"
      initialStorage={{
        // +++
        people: new LiveMap([
          [
            "alicia",
            new LiveObject({
              name: "Alicia",
              pets: new LiveList(["Fido", "Felix"]),
            }),
          ],
        ]),
        // +++
      }}
    >
      {/* children */}
    </RoomProvider>
  );
}
```

<Banner title="Need help troubleshooting Storage?">

Get the [Liveblocks DevTools extension](/devtools) to develop and debug your
application as you build it.

</Banner>

### useStorage [@badge=RoomProvider]

Extracts data from Liveblocks Storage state and automatically subscribes to
updates to that selected data. For full details, see [how selectors work][].

```tsx
// ✅ Rerenders if todos (or their children) change
const items = useStorage((root) => root.todos);

// ✅ Rerenders when todos are added or deleted
const numTodos = useStorage((root) => root.todos.length);

// ✅ Rerenders when the value of allDone changes
const allDone = useStorage((root) => root.todos.every((item) => item.done));

// ✅ Rerenders if any _unchecked_ todo items change
const uncheckedItems = useStorage(
  (root) => root.todos.filter((item) => !item.done),
  shallow // 👈
);
```

The `root` argument to the `useStorage` selector function is an _immutable_ copy
of your entire Liveblocks Storage tree. Think of it as the value you provided in
the `initialStorage` prop at the [`RoomProvider`][] level, but then
(recursively) converted to their “normal” JavaScript equivalents (objects,
arrays, maps) that are read-only.

From that immutable `root`, you can select or compute any value you like. Your
component will automatically get rerendered if the value you return differs from
the last rendered value.

This hook returns `null` while storage is still loading. To avoid that, use the
[Suspense version][].

<Banner type="success" title="Avoiding unnecessary rerenders">

It’s recommended to select only the subset of Storage data that your component
needs. This will avoid unnecessary rerenders that happen with overselection.

In order to select one item from a LiveMap within the storage tree with the
`useStorage` method, you can use the example below:

```ts
const key = "errands";
const myTodos = useStorage((root) => root.todoMap.get(key));
```

In order to query a LiveMap, and filter for specific values:

```ts
const myTodos = useStorage(
  root => Array.from(root.todoMap.values()).filter(...),
  shallow,
);
```

</Banner>

<PropertiesList title="Arguments">
  <PropertiesListItem name="selector" type="(root: ToImmutable<TStorage>) => T">
    A selector function to extract specific data from the storage root. The root
    is an immutable copy of your entire Liveblocks Storage tree.
  </PropertiesListItem>
  <PropertiesListItem name="isEqual" type="(prev: T, curr: T) => boolean">
    Optional equality function to determine if the selected data has changed.
    Defaults to strict equality comparison.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="data" type="T | null">
    The selected data from storage. Returns `null` while storage is still
    loading (in non-Suspense version).
  </PropertiesListItem>
</PropertiesList>

### useHistory [@badge=RoomProvider]

Returns the room’s history. See [`Room.history`][] for more information.

```ts
import { useHistory } from "@liveblocks/react/suspense";

const { undo, redo, pause, resume } = useHistory();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="history" type="History">
    The room's history object containing methods for undo, redo, pause, and
    resume operations.
  </PropertiesListItem>
</PropertiesList>

### useUndo [@badge=RoomProvider]

Returns a function that undoes the last operation executed by the current
client. It does not impact operations made by other clients.

```ts
import { useUndo } from "@liveblocks/react/suspense";

const undo = useUndo();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="undo" type="() => void">
    A function that undoes the last operation executed by the current client.
  </PropertiesListItem>
</PropertiesList>

### useRedo [@badge=RoomProvider]

Returns a function that redoes the last operation executed by the current
client. It does not impact operations made by other clients.

```ts
import { useRedo } from "@liveblocks/react/suspense";

const redo = useRedo();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="redo" type="() => void">
    A function that redoes the last operation executed by the current client.
  </PropertiesListItem>
</PropertiesList>

### useCanUndo [@badge=RoomProvider]

Returns whether there are any operations to undo.

```ts
import { useCanUndo, useUpdateMyPresence } from "@liveblocks/react/suspense";

const updateMyPresence = useUpdateMyPresence();
const canUndo = useCanUndo();

updateMyPresence({ y: 0 });

// At the next render, "canUndo" will be true
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="canUndo" type="boolean">
    Whether there are any operations to undo.
  </PropertiesListItem>
</PropertiesList>

### useCanRedo [@badge=RoomProvider]

Returns whether there are any operations to redo.

```ts
import {
  useCanRedo,
  useUndo,
  useUpdateMyPresence,
} from "@liveblocks/react/suspense";

const updateMyPresence = useUpdateMyPresence();
const undo = useUndo();
const canRedo = useCanRedo();

updateMyPresence({ y: 0 });
undo();

// At the next render, "canRedo" will be true
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

<PropertiesList title="Returns">
  <PropertiesListItem name="canRedo" type="boolean">
    Whether there are any operations to redo.
  </PropertiesListItem>
</PropertiesList>

### useMutation [@badge=RoomProvider]

Creates a callback function that lets you mutate Liveblocks state.

```tsx
import { useMutation } from "@liveblocks/react/suspense";

const fillWithRed = useMutation(
  // Mutation context is passed as the first argument
  ({ storage, setMyPresence }) => {
    // Mutate Storage
    storage.get("shapes").get("circle1").set("fill", "red");
    //                                   ^^^

    // ...or Presence
    setMyPresence({ lastUsedColor: "red" });
  },
  []
);

// JSX
return <button onClick={fillWithRed} />;
```

To make the example above more flexible and work with _any_ color, you have two
options:

1. Close over a local variable and
   [adding it to the dependency array](#useMutation-dep-arrays), or
2. Have it take [an extra callback parameter](#useMutation-extra-params).

Both are equally fine, just a matter of preference.

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="mutationFn"
    type="(context: MutationContext, ...args: TArgs) => void"
  >
    A function that performs mutations on Liveblocks state. The context provides
    access to `storage`, `setMyPresence`, `self`, and `others`.
  </PropertiesListItem>
  <PropertiesListItem name="deps" type="React.DependencyList">
    A dependency array that determines when the mutation function should be
    recreated, similar to `useCallback`.
  </PropertiesListItem>
</PropertiesList>

#### With dependency arrays [#useMutation-dep-arrays]

```tsx
// Local state maintained outside Liveblocks
const [currentColor, setCurrentColor] = useState("red");

const fillWithCurrentColor = useMutation(
  ({ storage, setMyPresence }) => {
    storage.get("shapes").get("circle1").set("fill", currentColor);
    setMyPresence({ lastUsedColor: currentColor });
  },
  // +++
  [currentColor] // Works just like it would in useCallback
  // +++
);

// JSX
return <button onClick={fillWithCurrentColor} />;
```

<Banner type="success" title="Tip! Let ESLint check your dependencies">

If you use ESLint, we recommend to
[configure it to enforce](/docs/api-reference/liveblocks-react#useMutation-lint-rule)
the correct use of your dependency arrays.

</Banner>

#### With extra callback parameters [#useMutation-extra-params]

Alternatively, you can add extra parameters to your callback function:

```tsx
const fill = useMutation(
  // +++
  // Note the second argument
  ({ storage, setMyPresence }, color: string) => {
    // +++
    storage.get("shapes").get("circle1").set("fill", color);
    setMyPresence({ lastUsedColor: color });
  },
  []
);

// JSX
// +++
return <button onClick={() => fill("red")} />;
//                            ^^^^^^^^^^^ Now fill takes a color argument
// +++
```

#### Depending on current presence [#useMutation-presence]

For convenience, the mutation context also receives `self` and `others`
arguments, which are _immutable_ values reflecting the current Presence state,
in case your mutation depends on it.

For example, here’s a mutation that will delete all the shapes selected by the
current user.

```tsx
const deleteSelectedShapes = useMutation(
  // You can use current "self" or "others" state in the mutation
  // +++
  ({ storage, self, others, setMyPresence }) => {
    // +++
    // Delete the selected shapes
    const shapes = storage.get("shapes");
    // +++
    for (const shapeId of self.presence.selectedShapeIds) {
      // +++
      shapes.delete(shapeId);
    }

    // Clear the current selection
    setMyPresence({ selectedShapeIds: [] });
  },
  []
);

// JSX
return <button onClick={deleteSelectedShapes} />;
```

Mutations are automatically batched, so when using `useMutation` there’s no need
to use `useBatch`, or call `room.batch()` manually.

#### ESLint rule [#useMutation-lint-rule] [@keywords=["exhaustive-deps", "additionalHooks", "eslint-plugin-react-hooks"]]

If you are using ESLint in your project, and are using
[the React hooks plugin](https://reactjs.org/docs/hooks-rules.html#eslint-plugin),
we recommend to add a check for "additional hooks", so that it will also check
the dependency arrays of your `useMutation` calls:

```js
{
  "rules": {
    // ...
    "react-hooks/exhaustive-deps": ["warn", {
      "additionalHooks": "useMutation"
    }]
  }
}
```

<PropertiesList title="Returns">
  <PropertiesListItem name="mutate" type="(...args: TArgs) => void">
    A memoized callback function that executes the mutation. Can accept
    additional arguments that are passed to the mutation function.
  </PropertiesListItem>
</PropertiesList>

## Comments

### useThreads [@badge=RoomProvider]

Returns a paginated list of threads within the current room. Initially fetches
the oldest 50 threads.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useThreads } from "@liveblocks/react";

const { threads, error, isLoading } = useThreads();
```

Use the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component to
render the oldest 50 threads with our default UI.

```tsx
import { Thread } from "@liveblocks/react-ui";
import { useThreads } from "@liveblocks/react/suspense";

function Component() {
  // +++
  const { threads } = useThreads();
  // +++

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

<PropertiesList title="Arguments">
  <PropertiesListItem name="options" type="object">
    Optional configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="option.query" type="ThreadsQuery">
    Optional query to filter threads by resolved status and metadata values.
    [Learn more](/docs/api-reference/liveblocks-react#useThreads-query).
  </PropertiesListItem>
  <PropertiesListItem name="option.scrollOnLoad" type="boolean">
    Whether to scroll to a comment if the URL's hash is set to a comment ID.
    Defaults to `true`. [Learn
    more](/docs/api-reference/liveblocks-react#useThreads-scroll).
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="threads" type="ThreadData[] | undefined">
    An array of threads within the current room, or `undefined` if not yet
    loaded (in non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the threads are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the threads.
  </PropertiesListItem>
  <PropertiesListItem name="hasFetchedAll" type="boolean">
    Whether all available threads have been fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useThreads-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMore" type="() => void">
    A function to fetch more threads. [Learn
    more](/docs/api-reference/liveblocks-react#useThreads-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="isFetchingMore" type="boolean">
    Whether more threads are currently being fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useThreads-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMoreError" type="Error | undefined">
    Any error that occurred while fetching more threads. [Learn
    more](/docs/api-reference/liveblocks-react#useThreads-pagination).
  </PropertiesListItem>
</PropertiesList>

#### Querying threads [#useThreads-query]

It’s possible to return threads that match a certain query with the `query`
option. You can filter threads based on their resolved status, if the user is
subscribed to them, and metadata. Additionally, you can filter for metadata
strings that being with certain characters using `startsWith` and you can filter
for metadata numbers using `gt`, `lt`, `gte`, and `lte`. Returned threads match
the entire query.

```tsx
// Returns threads that match the entire `query`, e.g. { color: "blue", pinned: true, ... }
const { threads } = useThreads({
  query: {
    // Filter for unresolved threads
    resolved: false,

    // Filter for threads that the user is subscribed to
    subscribed: true,

    metadata: {
      // Filter for threads that contain specific string, boolean, and number data
      color: "blue",
      pinned: true,
      priority: 3,

      // Filter for threads with string metadata that starts with a certain value
      organization: {
        startsWith: "liveblocks:",
      },

      // Filter for threads with number metadata that is greater than 50 and lower than 100
      posX: {
        gt: 50,
        lt: 100,
      },

      // Filter for threads with number metadata that is greater than or equal to 5
      level: {
        gte: 5,
      },
    },
  },
});
```

#### Pagination [#useThreads-pagination]

By default, the `useThreads` hook returns up to 50 threads. To fetch more, the
hook provides additional fields for pagination, similar to
[`useInboxNotifications`][].

```tsx
import { useThreads } from "@liveblocks/react";

const {
  isLoading,
  error,
  threads,

  +++
  fetchMore,
  isFetchingMore,
  hasFetchedAll,
  fetchMoreError,
  +++
} = useThreads();
```

- `hasFetchedAll` indicates whether all available threads have been fetched.
- `fetchMore` loads up to 50 more threads, and is always safe to call.
- `isFetchingMore` indicates whether more threads are being fetched.
- `fetchMoreError` returns error statuses resulting from fetching more.

##### Pagination example [#useThreads-pagination-example]

The following example demonstrates how to use the `fetchMore` function to
implement a “Load More” button, which fetches additional threads when clicked.
The button is disabled while fetching is in progress.

```tsx
import { Thread } from "@liveblocks/react-ui";
import { useThreads } from "@liveblocks/react/suspense";

function Threads() {
  const { threads, hasFetchedAll, fetchMore, isFetchingMore } = useThreads();

  return (
    <div>
      {threads.map((thread) => (
        <Thread key={thread.id} thread={thread} />
      ))}
      // +++
      {hasFetchedAll ? (
        <div>🎉 You've loaded all threads!</div>
      ) : (
        <button disabled={isFetchingMore} onClick={fetchMore}>
          Load more
        </button>
      )}
      // +++
    </div>
  );
}
```

#### Error handling [#useThreads-error-handling]

Error handling is another important aspect to consider when using the
`useThreads` hook. The `error` and `fetchMoreError` fields provide information
about any errors that occurred during the initial fetch or subsequent fetch
operations, respectively. You can use these fields to display appropriate error
messages to the user and implement retry mechanisms if needed.

The following example shows how to display error messages for both initial
loading errors and errors that occur when fetching more threads.

```tsx
import { Thread } from "@liveblocks/react-ui";
import { useThreads } from "@liveblocks/react";

function Inbox() {
  const { threads, error, fetchMore, fetchMoreError } = useThreads();

  // Handle error if the initial load failed.
  // The `error` field is not returned by the Suspense hook as the error is thrown to nearest ErrorBoundary
  // +++
  if (error) {
    return (
      <div>
        <p>Error loading threads: {error.message}</p>
      </div>
    );
  }
  // +++

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

      {fetchMoreError && (
        <div>
          <p>Error loading more threads: {fetchMoreError.message}</p>
          <button onClick={fetchMore}>Retry</button>
        </div>
      )}
    </div>
  );
}
```

#### Avoid scrolling to a comment [#useThreads-scroll]

By default, `scrollOnLoad`, is enabled. This options scrolls to a comment if the
URL’s hash is set to a comment ID (e.g.
`https://example.com/my-room#cm_nNJs9sb...`), the page will scroll to that
comment once the threads are loaded. To avoid scrolling to a comment, set
`scrollOnLoad` to `false`.

```tsx
const { threads } = useThreads({ scrollOnLoad: false });
```

### useCreateThread [@badge=RoomProvider]

Returns a function that optimistically creates a thread with an initial comment,
and optionally some thread and comment metadata.

```tsx
import { useCreateThread } from "@liveblocks/react/suspense";

const createThread = useCreateThread();
const thread = createThread({
  body: {},
  attachments: [],
  metadata: {},
  commentMetadata: {},
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="createThread"
    type="(options: CreateThreadOptions) => ThreadData"
  >
    A function that creates a thread with an initial comment, and optionally
    thread and comment metadata. Returns the optimistic thread object.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useCreateThread-error-handling]

`useCreateThread` creates threads optimistically, meaning that a thread object
is returned instantly, before Liveblocks has confirmed a successful thread
creation. To catch any errors that occur, add [`useErrorListener`][] and look
for the `CREATE_THREAD_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "CREATE_THREAD_ERROR") {
    const { roomId, threadId, commentId, body, metadata } = error.context;
    console.log(`Problem creating thread ${threadId}`);
  }
});
```

### useSearchComments [@badge=RoomProvider]

Search comments in the current room using semantic search and various filters.
Returns a list of results, including the ID and the plain text content of
matched comments, and the thread ID of the comment’s parent. Use it to
[create a search bar](#create-a-comments-search-bar).

```tsx
import { useSearchComments } from "@liveblocks/react";

const { results, error, isLoading } = useSearchComments({
  query: { text: "fruit" },
});

// [{ content: "I like apples", threadId: "th_xxx", commentId: "cm_xxx"  }, ...]
console.log(results);
```

Its semantic search finds results based on meaning, so a query like “fruit” also
brings up related items such as “apples” or “oranges”, even if the exact words
don’t match.

<PropertiesList title="Arguments">
  <PropertiesListItem name="options" type="object">
    Configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="options.query" type="SearchCommentsQuery">
    Optional query to filter comments by metadata and resolved status of the
    parent thread, and presence and absence of attachments and mentions in the
    comment.
  </PropertiesListItem>
  <PropertiesListItem name="options.query.text" type="string" required>
    Text to search within comment content. Uses rich text and vector search for
    relevance.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.query.threadMetadata"
    type="Partial<QueryMetadata>"
  >
    Metadata to filter threads by.
  </PropertiesListItem>
  <PropertiesListItem name="options.query.threadResolved" type="boolean">
    Whether to only return comments from threads that have attachments.
  </PropertiesListItem>
  <PropertiesListItem name="options.query.hasAttachments" type="boolean">
    Whether to only return comments that have attachments.
  </PropertiesListItem>
  <PropertiesListItem name="options.query.hasMentions" type="boolean">
    Whether to only return comments that have mentions.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="results" type="ThreadData[] | undefined">
    An array of matched comments with the parent thread they belong to.
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the results are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while searching the threads.
  </PropertiesListItem>
</PropertiesList>

#### Create a comments search bar

`useSearchComments` allows you to create a search bar for comments that exist
within the current room. Linking to the `commentId` with a hash will highlight
the comment on the page.

```tsx
import { useSearchComments } from "@liveblocks/react";
import { useState } from "react";

function SearchBar() {
  const [search, setSearch] = useState("");

  // +++
  const { results, isLoading, error } = useSearchComments({
    query: { text: search },
  });
  // +++

  return (
    <>
      <input
        type="text"
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />
      {isLoading ? (
        <div>Loading...</div>
      ) : (
        results.map((result) => (
          // +++
          <a key={result.commentId} href={"#" + result.commentId}>
            {result.content}
          </a>
          // +++
        ))
      )}
    </>
  );
}
```

You could also add buttons that apply various filters, such as “Resolved” and
“Unresolved”.

```tsx title="Resolved/unresolved filter" isCollapsable isCollapsed
import { useSearchComments } from "@liveblocks/react";
import { useState } from "react";

type ThreadResolved = "all" | "resolved" | "unresolved";

function SearchBar() {
  const [search, setSearch] = useState("");
  // +++
  const [searchResolved, searchResolved] = useState<ThreadResolved>("all");
  // +++

  // +++
  const searchThreadResolved =
    searchResolved === "all" ? undefined : searchResolved === "resolved";
  // +++

  const { results, isLoading, error } = useSearchComments({
    query: {
      text: search,
      // +++
      threadResolved: searchThreadResolved,
      // +++
    },
  });

  return (
    <>
      <input
        type="text"
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />
      // +++
      <button
        data-active={searchResolved === "all"}
        onClick={() => setSearchResolved("all")}
      >
        All
      </button>
      <button
        data-active={searchResolved === "resolved"}
        onClick={() => setSearchResolved("resolved")}
      >
        Resolved
      </button>
      <button
        data-active={searchResolved === "unresolved"}
        onClick={() => setSearchResolved("unresolved")}
      >
        Resolved
      </button>
      // +++
      {isLoading ? (
        <div>Loading...</div>
      ) : (
        results.map((result) => (
          <a key={result.commentId} href={"#" + result.commentId}>
            {result.content}
          </a>
        ))
      )}
    </>
  );
}
```

### useDeleteThread [@badge=RoomProvider]

Returns a function that deletes a thread and all its associated comments by ID.
Only the thread creator can delete the thread.

```tsx
import { useDeleteThread } from "@liveblocks/react/suspense";

const deleteThread = useDeleteThread();
deleteThread("th_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem name="deleteThread" type="(threadId: string) => void">
    A function that deletes a thread and all its associated comments by ID.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useDeleteThread-error-handling]

`useDeleteThread` deletes threads optimistically, meaning that the thread
appears deleted instantly, before Liveblocks has confirmed a successful thread
deletion. To catch any errors that occur, add [`useErrorListener`][] and look
for the `DELETE_THREAD_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "DELETE_THREAD_ERROR") {
    const { roomId, threadId } = error.context;
    console.log(`Problem deleting thread ${threadId}`);
  }
});
```

### useEditThreadMetadata [@badge=RoomProvider]

Returns a function that edits a thread’s metadata. To delete an existing
metadata property, set its value to `null`. Passing `undefined` for a metadata
property will ignore it.

```tsx
import { useEditThreadMetadata } from "@liveblocks/react/suspense";

const editThreadMetadata = useEditThreadMetadata();
editThreadMetadata({ threadId: "th_xxx", metadata: {} });
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="editThreadMetadata"
    type="(options: { threadId: string; metadata: ThreadMetadata }) => void"
  >
    A function that edits a thread’s metadata. To delete an existing metadata
    property, set its value to `null`.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useEditThreadMetadata-error-handling]

`useEditThreadMetadata` edits thread metadata optimistically, meaning that the
metadata appears updated instantly, before Liveblocks has confirmed a successful
metadata update. To catch any errors that occur, add [`useErrorListener`][] and
look for the `EDIT_THREAD_METADATA_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "EDIT_THREAD_METADATA_ERROR") {
    const { roomId, threadId, metadata } = error.context;
    console.log(`Problem editing thread metadata ${threadId}`);
  }
});
```

### useMarkThreadAsResolved [@badge=RoomProvider]

Returns a function that marks a thread as resolved.

```tsx
import { useMarkThreadAsResolved } from "@liveblocks/react/suspense";

const markThreadAsResolved = useMarkThreadAsResolved();
markThreadAsResolved("th_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="markThreadAsResolved"
    type="(threadId: string) => void"
  >
    A function that marks a thread as resolved.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useMarkThreadAsResolved-error-handling]

`useMarkThreadAsResolved` marks threads as resolved optimistically, meaning that
the thread appears resolved instantly, before Liveblocks has confirmed the
successful status change. To catch any errors that occur, add
[`useErrorListener`][] and look for the `MARK_THREAD_AS_RESOLVED_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "MARK_THREAD_AS_RESOLVED_ERROR") {
    const { roomId, threadId } = error.context;
    console.log(`Problem marking thread as resolved ${threadId}`);
  }
});
```

### useMarkThreadAsUnresolved [@badge=RoomProvider]

Returns a function that marks a thread as unresolved.

```tsx
import { useMarkThreadAsUnresolved } from "@liveblocks/react/suspense";

const markThreadAsUnresolved = useMarkThreadAsUnresolved();
markThreadAsUnresolved("th_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="markThreadAsUnresolved"
    type="(threadId: string) => void"
  >
    A function that marks a thread as unresolved.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useMarkThreadAsUnresolved-error-handling]

`useMarkThreadAsUnresolved` marks threads as unresolved optimistically, meaning
that the thread appears unresolved instantly, before Liveblocks has confirmed
the successful status change. To catch any errors that occur, add
[`useErrorListener`][] and look for the `MARK_THREAD_AS_UNRESOLVED_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "MARK_THREAD_AS_UNRESOLVED_ERROR") {
    const { roomId, threadId } = error.context;
    console.log(`Problem marking thread as unresolved ${threadId}`);
  }
});
```

### useMarkThreadAsRead [@badge=RoomProvider]

Returns a function that marks a thread as read.

```tsx
import { useMarkThreadAsRead } from "@liveblocks/react/suspense";

const markThreadAsRead = useMarkThreadAsRead();
markThreadAsRead("th_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem name="markThreadAsRead" type="(threadId: string) => void">
    A function that marks a thread as read.
  </PropertiesListItem>
</PropertiesList>

### useThreadSubscription [@badge=RoomProvider]

Returns the subscription status of a thread, methods to update it, and when the
thread was last read. The subscription status affects whether the current user
receives inbox notifications when new comments are posted.

```tsx
import { useThreadSubscription } from "@liveblocks/react/suspense";

const { status, subscribe, unsubscribe, unreadSince } =
  useThreadSubscription("th_xxx");
```

`subscribe` and `unsubscribe` work similarly to
[`useSubscribeToThread`](#useSubscribeToThread) and
[`useUnsubscribeFromThread`](#useUnsubscribeFromThread), but they only affect
the current thread.

<PropertiesList title="Arguments">
  <PropertiesListItem name="threadId" type="string">
    The ID of the thread to get subscription status for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="status" type="ThreadSubscriptionStatus">
    The subscription status of the thread ('subscribed', 'unsubscribed', or
    'not_subscribed').
  </PropertiesListItem>
  <PropertiesListItem name="subscribe" type="() => void">
    A function to subscribe to the thread.
  </PropertiesListItem>
  <PropertiesListItem name="unsubscribe" type="() => void">
    A function to unsubscribe from the thread.
  </PropertiesListItem>
  <PropertiesListItem name="unreadSince" type="Date | null">
    The date when the thread was last read, or null if it has been read.
  </PropertiesListItem>
</PropertiesList>

### useSubscribeToThread [@badge=RoomProvider]

Returns a function that subscribes the current user to a thread, meaning they
will receive inbox notifications when new comments are posted.

```tsx
import { useSubscribeToThread } from "@liveblocks/react/suspense";

const subscribeToThread = useSubscribeToThread();
subscribeToThread("th_xxx");
```

#### Error handling [#useSubscribeToThread-error-handling]

`useSubscribeToThread` subscribes to threads optimistically, meaning that the
subscription appears active instantly, before Liveblocks has confirmed a
successful subscription. To catch any errors that occur, add
[`useErrorListener`][] and look for the `SUBSCRIBE_TO_THREAD_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "SUBSCRIBE_TO_THREAD_ERROR") {
    const { roomId, threadId } = error.context;
    console.log(`Problem subscribing to thread ${threadId}`);
  }
});
```

Subscribing will replace any existing subscription for the current thread
[set at room-level](#useRoomSubscriptionSettings). This value can also be
overridden by a room-level call that is run afterwards.

```ts
const subscribeToThread = useSubscribeToThread();
const [{ settings }, updateSettings] = useRoomSubscriptionSettings();

// 1. Disables notifications for all threads
updateSettings({
  threads: "none",
});

// 2. Enables notifications just for this thread, "th_d75sF3..."
subscribeToThread("th_d75sF3...");

// 3. Disables notifications for all threads, including "th_d75sF3..."
updateSettings({
  threads: "none",
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="subscribeToThread"
    type="(threadId: string) => void"
  >
    A function that subscribes the current user to a thread for inbox
    notifications.
  </PropertiesListItem>
</PropertiesList>

### useUnsubscribeFromThread [@badge=RoomProvider]

Returns a function that unsubscribes the current user from a thread, meaning
they will no longer receive inbox notifications when new comments are posted.

```tsx
import { useUnsubscribeFromThread } from "@liveblocks/react/suspense";

const unsubscribeFromThread = useUnsubscribeFromThread();
unsubscribeFromThread("th_xxx");
```

Unsubscribing will replace any existing subscription for the current thread
[set at room-level](#useRoomSubscriptionSettings). This value can also be
overridden by a room-level call that is run afterwards.

```ts
const subscribeToThread = useSubscribeToThread();
const [{ settings }, updateSettings] = useRoomSubscriptionSettings();

// 1. Enables notifications for all threads
updateSettings({
  threads: "all",
});

// 2. Disables notifications just for this thread, "th_d75sF3..."
subscribeToThread("th_d75sF3...");

// 3. Enables notifications for all threads, including "th_d75sF3..."
updateSettings({
  threads: "all",
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="unsubscribeFromThread"
    type="(threadId: string) => void"
  >
    A function that unsubscribes the current user from a thread, stopping inbox
    notifications.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useUnsubscribeFromThread-error-handling]

`useUnsubscribeFromThread` unsubscribes from threads optimistically, meaning
that the subscription appears inactive instantly, before Liveblocks has
confirmed a successful unsubscription. To catch any errors that occur, add
[`useErrorListener`][] and look for the `UNSUBSCRIBE_FROM_THREAD_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "UNSUBSCRIBE_FROM_THREAD_ERROR") {
    const { roomId, threadId } = error.context;
    console.log(`Problem unsubscribing from thread ${threadId}`);
  }
});
```

### useCreateComment [@badge=RoomProvider]

Returns a function that adds a comment to a thread.

```tsx
import { useCreateComment } from "@liveblocks/react/suspense";

const createComment = useCreateComment();
const comment = createComment({
  threadId: "th_xxx",
  body: {},
  attachments: [],
  metadata: {},
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="createComment"
    type="(options: CreateCommentOptions) => CommentData"
  >
    A function that adds a comment to a thread. Returns the optimistic comment
    object.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useCreateComment-error-handling]

`useCreateComment` creates comments optimistically, meaning that a comment
object is returned instantly, before Liveblocks has confirmed a successful
comment creation. To catch any errors that occur, add [`useErrorListener`][] and
look for the `CREATE_COMMENT_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "CREATE_COMMENT_ERROR") {
    const { roomId, threadId, commentId, body } = error.context;
    console.log(`Problem creating comment ${commentId}`);
  }
});
```

### useEditComment[@badge=RoomProvider]

Returns a function that edits a comment’s body, and optionally its attachments
and metadata.

```tsx
import { useEditComment } from "@liveblocks/react/suspense";

const editComment = useEditComment();
editComment({
  threadId: "th_xxx",
  commentId: "cm_xxx",
  body: {},
  attachments: [],
  metadata: {},
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="editComment"
    type="(options: EditCommentOptions) => void"
  >
    A function that edits a comment’s body, and optionally its attachments and
    metadata.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useEditComment-error-handling]

`useEditComment` edits comments optimistically, meaning that the comment appears
updated instantly, before Liveblocks has confirmed a successful comment edit. To
catch any errors that occur, add [`useErrorListener`][] and look for the
`EDIT_COMMENT_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "EDIT_COMMENT_ERROR") {
    const { roomId, threadId, commentId, body } = error.context;
    console.log(`Problem editing comment ${commentId}`);
  }
});
```

### useEditCommentMetadata [@badge=RoomProvider]

Returns a function that edits a comment’s metadata. To delete an existing
metadata property, set its value to `null`. Passing `undefined` for a metadata
property will ignore it.

```tsx
import { useEditCommentMetadata } from "@liveblocks/react/suspense";

const editCommentMetadata = useEditCommentMetadata();
editCommentMetadata({
  threadId: "th_xxx",
  commentId: "cm_xxx",
  metadata: {},
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="editCommentMetadata"
    type="(options: { threadId: string; commentId: string; metadata: CommentMetadata }) => void"
  >
    A function that edits a comment’s metadata. To delete an existing metadata
    property, set its value to `null`.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useEditCommentMetadata-error-handling]

`useEditCommentMetadata` edits comment metadata optimistically, meaning that the
metadata appears updated instantly, before Liveblocks has confirmed a successful
metadata update. To catch any errors that occur, add [`useErrorListener`][] and
look for the `EDIT_COMMENT_METADATA_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "EDIT_COMMENT_METADATA_ERROR") {
    const { roomId, threadId, commentId, metadata } = error.context;
    console.log(`Problem editing comment metadata ${commentId}`);
  }
});
```

### useDeleteComment [@badge=RoomProvider]

Returns a function that deletes a comment. If it is the last non-deleted
comment, the thread also gets deleted.

```tsx
import { useDeleteComment } from "@liveblocks/react/suspense";

const deleteComment = useDeleteComment();
deleteComment({ threadId: "th_xxx", commentId: "cm_xxx" });
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="deleteComment"
    type="(options: { threadId: string; commentId: string }) => void"
  >
    A function that deletes a comment. If it is the last non-deleted comment,
    the thread also gets deleted.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useDeleteComment-error-handling]

`useDeleteComment` deletes comments optimistically, meaning that the comment
appears deleted instantly, before Liveblocks has confirmed a successful comment
deletion. To catch any errors that occur, add [`useErrorListener`][] and look
for the `DELETE_COMMENT_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "DELETE_COMMENT_ERROR") {
    const { roomId, threadId, commentId } = error.context;
    console.log(`Problem deleting comment ${commentId}`);
  }
});
```

### useAddReaction [@badge=RoomProvider]

Returns a function that adds a reaction to a comment. Can be used to create an
[emoji picker](/docs/api-reference/liveblocks-react-ui#emoji-picker) or
[emoji reactions](/docs/api-reference/liveblocks-react-ui#emoji-reactions).

```tsx
import { useAddReaction } from "@liveblocks/react/suspense";

const addReaction = useAddReaction();
addReaction({ threadId: "th_xxx", commentId: "cm_xxx", emoji: "👍" });
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="addReaction"
    type="(options: { threadId: string; commentId: string; emoji: string }) => void"
  >
    A function that adds a reaction to a comment.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useAddReaction-error-handling]

`useAddReaction` adds reactions optimistically, meaning that the reaction
appears instantly, before Liveblocks has confirmed a successful reaction
addition. To catch any errors that occur, add [`useErrorListener`][] and look
for the `ADD_REACTION_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "ADD_REACTION_ERROR") {
    const { roomId, threadId, commentId, emoji } = error.context;
    console.log(`Problem adding reaction ${emoji} to comment ${commentId}`);
  }
});
```

### useRemoveReaction [@badge=RoomProvider]

Returns a function that removes a reaction from a comment. Can be used to create
an [emoji picker](/docs/api-reference/liveblocks-react-ui#emoji-picker) or
[emoji reactions](/docs/api-reference/liveblocks-react-ui#emoji-reactions)

```tsx
import { useRemoveReaction } from "@liveblocks/react/suspense";

const removeReaction = useRemoveReaction();
removeReaction({ threadId: "th_xxx", commentId: "cm_xxx", emoji: "👍" });
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="removeReaction"
    type="(options: { threadId: string; commentId: string; emoji: string }) => void"
  >
    A function that removes a reaction from a comment.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useRemoveReaction-error-handling]

`useRemoveReaction` removes reactions optimistically, meaning that the reaction
disappears instantly, before Liveblocks has confirmed a successful reaction
removal. To catch any errors that occur, add [`useErrorListener`][] and look for
the `REMOVE_REACTION_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "REMOVE_REACTION_ERROR") {
    const { roomId, threadId, commentId, emoji } = error.context;
    console.log(`Problem removing reaction ${emoji} from comment ${commentId}`);
  }
});
```

### useAttachmentUrl [@badge=RoomProvider]

Returns a presigned URL for an attachment by its ID.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useAttachmentUrl } from "@liveblocks/react";

const { url, error, isLoading } = useAttachmentUrl("at_xxx");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="attachmentId" type="string">
    The ID of the attachment to get a presigned URL for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="url" type="string | undefined">
    The presigned URL for the attachment, or `undefined` if not found or not yet
    loaded (in non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the URL is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the URL.
  </PropertiesListItem>
</PropertiesList>

## Notifications

### useInboxNotifications [@badge=LiveblocksProvider]

Returns a paginated list of inbox notifications for the current user. Initially
fetches the latest 50 items. Inbox notifications are
[project-based](/docs/ready-made-features/notifications/concepts#Project-based),
meaning notifications from outside the current room are received.

```tsx
import { useInboxNotifications } from "@liveblocks/react";

const { inboxNotifications, error, isLoading } = useInboxNotifications();
```

Use the
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification)
component to render the latest 50 inbox notifications with our default UI.

```tsx
import { InboxNotification } from "@liveblocks/react-ui";
import { useInboxNotifications } from "@liveblocks/react/suspense";

function Inbox() {
  // +++
  const { inboxNotifications } = useInboxNotifications();
  // +++

  return (
    <div>
      // +++
      {inboxNotifications.map((notification) => (
        <InboxNotification
          key={notification.id}
          inboxNotification={notification}
        />
      ))}
      // +++
    </div>
  );
}
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="options" type="object">
    Optional configuration object.
  </PropertiesListItem>
  <PropertiesListItem name="options.query" type="InboxNotificationsQuery">
    Optional query to filter notifications by room ID or kind. [Learn
    more](/docs/api-reference/liveblocks-react#useInboxNotifications-query).
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem
    name="inboxNotifications"
    type="InboxNotificationData[] | undefined"
  >
    An array of inbox notifications for the current user, or `undefined` if not
    yet loaded (in non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the notifications are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the notifications.
  </PropertiesListItem>
  <PropertiesListItem name="hasFetchedAll" type="boolean">
    Whether all available notifications have been fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMore" type="() => void">
    A function to fetch more notifications. [Learn
    more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="isFetchingMore" type="boolean">
    Whether more notifications are currently being fetched. [Learn
    more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination).
  </PropertiesListItem>
  <PropertiesListItem name="fetchMoreError" type="Error | undefined">
    Any error that occurred while fetching more notifications. [Learn
    more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination).
  </PropertiesListItem>
</PropertiesList>

#### Querying inbox notifications [#useInboxNotifications-query]

It’s possible to return inbox notifications that match a certain query with the
`query` option. You can filter inbox notifications based on their associated
room ID or kind.

```tsx
// Returns inbox notifications that match the entire `query`, e.g. { roomId: "room1", ... }
const { inboxNotifications } = useInboxNotifications({
  query: {
    // Filter for roomId
    roomId: "room1",
    // Filter for kind
    kind: "thread",
  },
});
```

#### Pagination [#useInboxNotifications-pagination]

By default, the `useInboxNotifications` hook returns up to 50 notifications. To
fetch more, the hook provides additional fields for pagination, similar to
[`useThreads`][].

```tsx
import { useInboxNotifications } from "@liveblocks/react";

const {
  inboxNotifications,
  isLoading,
  error,

  +++
  hasFetchedAll,
  fetchMore,
  isFetchingMore,
  fetchMoreError,
  +++
} = useInboxNotifications();
```

- `hasFetchedAll` indicates whether all available inbox notifications have been
  fetched.
- `fetchMore` loads up to 50 more notifications, and is always safe to call.
- `isFetchingMore` indicates whether more notifications are being fetched.
- `fetchMoreError` returns error statuses resulting from fetching more.

##### Pagination example [#useInboxNotifications-pagination-example]

The following example demonstrates how to use the `fetchMore` function to
implement a “Load More” button, which fetches additional inbox notifications
when clicked. The button is disabled while fetching is in progress.

```tsx
import { InboxNotification } from "@liveblocks/react-ui";
import { useInboxNotifications } from "@liveblocks/react/suspense";

function Inbox() {
  const { inboxNotifications, hasFetchedAll, fetchMore, isFetchingMore } =
    useInboxNotifications();

  return (
    <div>
      {inboxNotifications.map((notification) => (
        <InboxNotification
          key={notification.id}
          inboxNotification={notification}
        />
      ))}
      // +++
      {hasFetchedAll ? (
        <div>🎉 You're all caught up!</div>
      ) : (
        <button disabled={isFetchingMore} onClick={fetchMore}>
          Load more
        </button>
      )}
      // +++
    </div>
  );
}
```

#### Error handling [#useInboxNotifications-error-handling]

Error handling is another important aspect to consider when using the
`useInboxNotifications` hook. The `error` and `fetchMoreError` fields provide
information about any errors that occurred during the initial fetch or
subsequent fetch operations, respectively. You can use these fields to display
appropriate error messages to the user and implement retry mechanisms if needed.

The following example shows how to display error messages for both initial
loading errors and errors that occur when fetching more inbox notifications.

```tsx
import { InboxNotification } from "@liveblocks/react-ui";
import { useInboxNotifications } from "@liveblocks/react";

function Inbox() {
  const { inboxNotifications, error, fetchMore, fetchMoreError } =
    useInboxNotifications();

  // Handle error if the initial load failed.
  // The `error` field is not returned by the Suspense hook as the error is thrown to nearest ErrorBoundary
  // +++
  if (error) {
    return (
      <div>
        <p>Error loading inbox notifications: {error.message}</p>
      </div>
    );
  }
  // +++

  return (
    <div>
      {inboxNotifications.map((notification) => (
        <InboxNotification
          key={notification.id}
          inboxNotification={notification}
        />
      ))}

      {fetchMoreError && (
        <div>
          <p>
            Error loading more inbox notifications: {fetchMoreError.message}
          </p>
          <button onClick={fetchMore}>Retry</button>
        </div>
      )}
    </div>
  );
}
```

#### Batched notifications [#useInboxNotifications-batched-notifications]

If you’re
[batching custom notifications](/docs/api-reference/liveblocks-node#Batching-custom-notifications),
you can render each activity inside a single notification. The `activities`
array will contain multiple items.

```tsx
import { InboxNotification } from "@liveblocks/react-ui";
import { useInboxNotifications } from "@liveblocks/react/suspense";

function Inbox() {
  const { inboxNotifications } = useInboxNotifications();

  // If the last notification was batched, it will have multiple
  // items in the `activities` array
  // {
  //   id: "in_3dH7sF3...",
  //   kind: "$fileUploaded",
  // +++
  //   activities: [
  //     { status: "processing" },
  //     { status: "complete" },
  //   ],
  // +++
  //   ...
  // }
  console.log(inboxNotifications[0].activities);

  // ...
}
```

### useUnreadInboxNotificationsCount [@badge=LiveblocksProvider]

Returns the number of unread inbox notifications for the current user.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useUnreadInboxNotificationsCount } from "@liveblocks/react";

const { count, error, isLoading } = useUnreadInboxNotificationsCount();
```

<PropertiesList title="Arguments">
  <PropertiesListItem
    name="options"
    type="UseUnreadInboxNotificationsCountOptions"
  >
    Optional configuration object.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="query" type="InboxNotificationsQuery">
    Optional query to filter notifications count by room ID or kind.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="count" type="number | undefined">
    The number of unread inbox notifications for the current user, or
    `undefined` if not yet loaded (in non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the number of unread inbox notifications is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the number of unread inbox
    notifications.
  </PropertiesListItem>
</PropertiesList>

#### Querying unread inbox notifications count

It’s possible to return the count of unread inbox notifications that match a
certain query with the `query` option. You can filter unread inbox notifications
count based on their associated room ID or kind.

```tsx
// Returns the count that match the entire `query`, e.g. { roomId: "room1", ... }
const { count } = useUnreadInboxNotificationsCount({
  query: {
    // Filter for roomId
    roomId: "room1",
    // Filter for kind
    kind: "thread",
  },
});
```

### useMarkInboxNotificationAsRead [@badge=LiveblocksProvider]

Returns a function that marks an inbox notification as read for the current
user.

```tsx
import { useMarkInboxNotificationAsRead } from "@liveblocks/react/suspense";

const markInboxNotificationAsRead = useMarkInboxNotificationAsRead();
markInboxNotificationAsRead("in_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="markInboxNotificationAsRead"
    type="(inboxNotificationId: string) => void"
  >
    A function that marks an inbox notification as read for the current user.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useMarkInboxNotificationAsRead-error-handling]

`useMarkInboxNotificationAsRead` marks notifications as read optimistically,
meaning that the notification appears read instantly, before Liveblocks has
confirmed a successful status change. To catch any errors that occur, add
[`useErrorListener`][] and look for the `MARK_INBOX_NOTIFICATION_AS_READ_ERROR`
type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "MARK_INBOX_NOTIFICATION_AS_READ_ERROR") {
    const { inboxNotificationId, roomId } = error.context;
    console.log(`Problem marking notification as read ${inboxNotificationId}`);
  }
});
```

### useMarkAllInboxNotificationsAsRead [@badge=LiveblocksProvider]

Returns a function that marks all of the current user‘s inbox notifications as
read.

```tsx
import { useMarkAllInboxNotificationsAsRead } from "@liveblocks/react/suspense";

const markAllInboxNotificationsAsRead = useMarkAllInboxNotificationsAsRead();
markAllInboxNotificationsAsRead();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="markAllInboxNotificationsAsRead" type="() => void">
    A function that marks all of the current user's inbox notifications as read.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useMarkAllInboxNotificationsAsRead-error-handling]

`useMarkAllInboxNotificationsAsRead` marks all notifications as read
optimistically, meaning that the notifications appear read instantly, before
Liveblocks has confirmed a successful status change. To catch any errors that
occur, add [`useErrorListener`][] and look for the
`MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR") {
    console.log("Problem marking all notifications as read");
  }
});
```

### useDeleteInboxNotification [@badge=LiveblocksProvider]

Returns a function that deletes an inbox notification for the current user.

```tsx
import { useDeleteInboxNotification } from "@liveblocks/react/suspense";

const deleteInboxNotification = useDeleteInboxNotification();
deleteInboxNotification("in_xxx");
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="deleteInboxNotification"
    type="(inboxNotificationId: string) => void"
  >
    A function that deletes an inbox notification for the current user.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useDeleteInboxNotification-error-handling]

`useDeleteInboxNotification` deletes notifications optimistically, meaning that
the notification appears deleted instantly, before Liveblocks has confirmed a
successful deletion. To catch any errors that occur, add [`useErrorListener`][]
and look for the `DELETE_INBOX_NOTIFICATION_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "DELETE_INBOX_NOTIFICATION_ERROR") {
    const { inboxNotificationId } = error.context;
    console.log(`Problem deleting notification ${inboxNotificationId}`);
  }
});
```

### useDeleteAllInboxNotifications [@badge=LiveblocksProvider]

Returns a function that deletes all of the current user‘s inbox notifications.

```tsx
import { useDeleteAllInboxNotifications } from "@liveblocks/react/suspense";

const deleteAllInboxNotifications = useDeleteAllInboxNotifications();
deleteAllInboxNotifications();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="deleteAllInboxNotifications" type="() => void">
    A function that deletes all of the current user’s inbox notifications.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useDeleteAllInboxNotifications-error-handling]

`useDeleteAllInboxNotifications` deletes all notifications optimistically,
meaning that the notifications appear deleted instantly, before Liveblocks has
confirmed a successful deletion. To catch any errors that occur, add
[`useErrorListener`][] and look for the `DELETE_ALL_INBOX_NOTIFICATIONS_ERROR`
type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR") {
    console.log("Problem deleting all notifications");
  }
});
```

### useInboxNotificationThread [@badge=LiveblocksProvider]

Returns the thread associated with a `"thread"` inbox notification.

```tsx
import { useInboxNotificationThread } from "@liveblocks/react/suspense";

const thread = useInboxNotificationThread("in_xxx");
```

It can **only** be called with IDs of `"thread"` inbox notifications, so we
recommend only using it
[when customizing the rendering](/docs/api-reference/liveblocks-react-ui#Rendering-notification-kinds-differently)
or in other situations where you can guarantee the kind of the notification.

<Banner type="info" title="No fetching and waterfalls">

When `useInboxNotifications` returns `"thread"` inbox notifications, it also
receives the associated threads and caches them behind the scenes. When you call
`useInboxNotificationThread`, it simply returns the cached thread for the inbox
notification ID you passed to it, without any fetching or waterfalls.

</Banner>

<PropertiesList title="Arguments">
  <PropertiesListItem name="inboxNotificationId" type="string">
    The ID of the inbox notification to get the associated thread for. Must be a
    "thread" type notification.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="thread" type="ThreadData | null">
    The thread associated with the inbox notification, or null if not found.
  </PropertiesListItem>
</PropertiesList>

### useRoomSubscriptionSettings [@badge=RoomProvider]

Returns the user’s subscription settings for the current room and a function to
update them. Updating this setting will change which
[`inboxNotifications`](#useInboxNotifications) the current user receives in the
current room.

```tsx
import { useRoomSubscriptionSettings } from "@liveblocks/react/suspense";

const [{ settings }, updateSettings] = useRoomSubscriptionSettings();

// { threads: "replies_and_mentions", textMentions: "mine" }
console.log(settings);

// No longer receive thread subscriptions in this room
updateSettings({
  threads: "none",
});
```

For `"threads"`, these are the three possible values that can be set:

- `"all"` Receive notifications for every activity in every thread.
- `"replies_and_mentions"` Receive notifications for mentions and threads you’re
  participating in.
- `"none"` No notifications are received.

For `"textMentions"`, these are the two possible values that can be set:

- `"mine"` Receive notifications for mentions of you.
- `"none"` No notifications are received.

<PropertiesList title="Returns">
  <PropertiesListItem name="settings" type="RoomSubscriptionSettings">
    The current subscription settings for the room.
  </PropertiesListItem>
  <PropertiesListItem
    name="updateSettings"
    type="(settings: Partial<RoomSubscriptionSettings>) => void"
  >
    A function to update the subscription settings.
  </PropertiesListItem>
</PropertiesList>

### useUpdateRoomSubscriptionSettings [@badge=RoomProvider]

Returns a function that updates the user’s notification settings for the current
room. Updating this setting will change which
[`inboxNotifications`](#useInboxNotifications) the current user receives in the
current room.

```tsx
import { useUpdateRoomSubscriptionSettings } from "@liveblocks/react/suspense";

const updateRoomSubscriptionSettings = useUpdateRoomSubscriptionSettings();

// No longer receive thread notifications in this room
updateSettings({
  threads: "none",
});
```

#### Error handling [#useUpdateRoomSubscriptionSettings-error-handling]

`useUpdateRoomSubscriptionSettings` updates subscription settings
optimistically, meaning that the settings appear updated instantly, before
Liveblocks has confirmed a successful update. To catch any errors that occur,
add [`useErrorListener`][] and look for the
`UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR") {
    const { roomId } = error.context;
    console.log(`Problem updating subscription settings for room ${roomId}`);
  }
});
```

For `"threads"`, these are the three possible values that can be set:

- `"all"` Receive notifications for every activity in every thread.
- `"replies_and_mentions"` Receive notifications for mentions and threads you're
  participating in.
- `"none"` No notifications are received.

For `"textMentions"`, these are the two possible values that can be set:

- `"mine"` Receive notifications for mentions of you.
- `"none"` No notifications are received.

Works the same as `updateSettings` in
[`useRoomSubscriptionSettings`](#useRoomSubscriptionSettings).

<PropertiesList title="Returns">
  <PropertiesListItem
    name="updateRoomSubscriptionSettings"
    type="(settings: Partial<RoomSubscriptionSettings>) => void"
  >
    A function that updates the user's notification settings for the current
    room.
  </PropertiesListItem>
</PropertiesList>

##### Replacing individual thread subscriptions

Subscribing will replace any
[existing thread subscriptions](#useSubscribeToThread) in the current room. This
value can also be overridden by a room-level call that is run afterwards.

```ts
const subscribeToThread = useSubscribeToThread();
const [{ settings }, updateSettings] = useRoomSubscriptionSettings();

// 1. Enables notifications just for this thread, "th_d75sF3..."
subscribeToThread("th_d75sF3...");

// 2. Disables notifications for all threads, including "th_d75sF3..."
updateSettings({
  threads: "none",
});
```

#### Error handling [#useUpdateRoomSubscriptionSettings-error-handling]

`useUpdateRoomSubscriptionSettings` updates subscription settings
optimistically, meaning that the settings appear updated instantly, before
Liveblocks has confirmed a successful update. To catch any errors that occur,
add [`useErrorListener`][] and look for the
`UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR` type.

```tsx
import { useErrorListener } from "@liveblocks/react/suspense";

useErrorListener((error) => {
  if (error.context.type === "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR") {
    const { roomId } = error.context;
    console.log(`Problem updating subscription settings for room ${roomId}`);
  }
});
```

### useNotificationSettings [@badge=LiveblocksProvider]

<Banner>

Notification settings is currently in beta.

</Banner>

Returns the user’s notification settings in the current project, in other words
which [notification webhook events](/docs/platform/webhooks#NotificationEvent)
will be sent for the current user. Notification settings are project-based,
which means that `settings` and `updateSettings` are for the current user’s
settings in every room. Useful for creating a
[notification settings panel](/docs/guides/how-to-create-a-notification-settings-panel).

```tsx
import { useNotificationSettings } from "@liveblocks/react";

const [{ isLoading, error, settings }, updateSettings] =
  useNotificationSettings();

// Current user receives thread notifications on the email channel
// { email: { thread: true, ... }, ... }
console.log(settings);

// Disabling thread notifications on the email channel
updateSettings({
  email: {
    thread: false,
  },
});
```

A user’s initial settings are set in the dashboard, and different kinds should
be enabled there. If no kind is enabled on the current channel, `null` will be
returned. For example, with the email channel:

```ts
const [{ isLoading, error, settings }, updateSettings] =
  useNotificationSettings();

// { email: null, ... }
console.log(settings);
```

#### Updating notification settings [#useNotificationSettings-updating-notification-settings]

The `updateSettings` function can be used to update the current user’s
notification settings, changing their settings for every room in the project.
Each notification `kind` must first be enabled on your project’s notification
dashboard page before settings can be used.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
// You only need to pass partials
updateSettings({
  email: { thread: true },
});

// Enabling a custom notification on the slack channel
updateSettings({
  slack: { $myCustomNotification: true },
});

// Setting complex settings
updateSettings({
  email: {
    thread: true,
    textMention: false,
    $newDocument: true,
  },
  slack: {
    thread: false,
    $fileUpload: false,
  },
  teams: {
    thread: true,
  },
});
```

Subscribing will replace any
[existing thread subscriptions](#useSubscribeToThread) in the current room. This
value can also be overridden by a room-level call that is run afterwards.

```ts
const subscribeToThread = useSubscribeToThread();
const [{ settings }, updateSettings] = useRoomSubscriptionSettings();

// 1. Enables notifications just for this thread, "th_d75sF3..."
subscribeToThread("th_d75sF3...");

// 2. Disables notifications for all threads, including "th_d75sF3..."
updateSettings({
  threads: "none",
});
```

<PropertiesList title="Returns">
  <PropertiesListItem name="settings" type="NotificationSettings | null">
    The current notification settings for the user, or null if no settings are
    configured.
  </PropertiesListItem>
  <PropertiesListItem
    name="updateSettings"
    type="(settings: Partial<NotificationSettings>) => void"
  >
    A function to update the notification settings.
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the notification settings are currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | null">
    Any error that occurred while loading the notification settings.
  </PropertiesListItem>
</PropertiesList>

#### Error handling [#useNotificationSettings-error-handling]

Error handling is an important aspect to consider when using the
`useNotificationSettings` hook. The `error` fields provides information about
any error that occurred during the fetch operation.

The following example shows how to display error messages for both initial
loading errors and errors that occur when fetching more inbox notifications.

```tsx
import { useNotificationSettings } from "@liveblocks/react";

const [{ isLoading, error, settings }, updateSettings] =
  useNotificationSettings();

if (error) {
  return (
    <div>
      <p>Error loading notification settings: {error.message}</p>
    </div>
  );
}
```

### useUpdateNotificationSettings [@badge=LiveblocksProvider]

<Banner>

Notification settings is currently in beta.

</Banner>

Returns a function that updates user’s notification settings, which affects
which [notification webhook events](/docs/platform/webhooks#NotificationEvent)
will be sent for the current user. Notification settings are project-based,
which means that `updateSettings` modifies the current user’s settings in every
room. Each notification `kind` must first be enabled on your project’s
notification dashboard page before settings can be used. Useful for creating a
[notification settings panel](/docs/guides/how-to-create-a-notification-settings-panel).

```tsx
import { useUpdateNotificationSettings } from "@liveblocks/react";

const updateSettings = useUpdateNotificationSettings();

// Disabling thread notifications on the email channel
updateSettings({
  email: { thread: false },
});
```

Works the same as `updateSettings` in
[`useNotificationSettings`](#useNotificationSettings). You can pass a partial
object, or many settings at once.

```tsx
// You only need to pass partials
updateSettings({
  email: { thread: true },
});

// Enabling a custom notification on the slack channel
updateSettings({
  slack: { $myCustomNotification: true },
});

// Setting complex settings
updateSettings({
  email: {
    thread: true,
    textMention: false,
    $newDocument: true,
  },
  slack: {
    thread: false,
    $fileUpload: false,
  },
  teams: {
    thread: true,
  },
});
```

<PropertiesList title="Returns">
  <PropertiesListItem
    name="updateNotificationSettings"
    type="(settings: Partial<NotificationSettings>) => void"
  >
    A function that updates the user's notification settings for the current
    project.
  </PropertiesListItem>
</PropertiesList>

## Version History

### useHistoryVersions [@badge=RoomProvider]

Returns the versions of the room. See
[Version History Components](/docs/api-reference/liveblocks-react-ui#Version-history-components)
for more information on how to display versions.

```tsx
import { useHistoryVersions } from "@liveblocks/react";

const { versions, error, isLoading } = useHistoryVersions();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="versions" type="HistoryVersion[]">
    An array of history versions for the room.
  </PropertiesListItem>
</PropertiesList>

## Miscellaneous

### useUser [@badge=Both]

Returns user info from a given user ID. To use `useUser`, you should provide a
resolver function to the [`resolveUsers`][] option in [`createClient`][] or
[`LiveblocksProvider`][].
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useUser } from "@liveblocks/react";

const { user, error, isLoading } = useUser("user-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="userId" type="string">
    The ID of the user to get information for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="user" type="User | undefined">
    The user information, or `undefined` if not found or not yet loaded (in
    non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the user information is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the user information.
  </PropertiesListItem>
</PropertiesList>

### useRoomInfo [@badge=Both]

Returns room info from a given room ID. To use `useRoomInfo`, you should provide
a resolver function to the [`resolveRoomsInfo`][] option in [`createClient`][]
or [`LiveblocksProvider`][].
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useRoomInfo } from "@liveblocks/react";

const { info, error, isLoading } = useRoomInfo("room-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="roomId" type="string">
    The ID of the room to get information for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="info" type="RoomInfo | undefined">
    The room information, or `undefined` if not found or not yet loaded (in
    non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the room information is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the room information.
  </PropertiesListItem>
</PropertiesList>

### useGroupInfo [@badge=Both]

Returns group info from a given group ID. To use `useGroupInfo`, you should
provide a resolver function to the [`resolveGroupsInfo`][] option in
[`createClient`][] or [`LiveblocksProvider`][].
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useGroupInfo } from "@liveblocks/react";

const { info, error, isLoading } = useGroupInfo("group-id");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="groupId" type="string">
    The ID of the group to get information for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="info" type="GroupInfo | undefined">
    The group information, or `undefined` if not found or not yet loaded (in
    non-Suspense version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the group information is currently being loaded.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the group information.
  </PropertiesListItem>
</PropertiesList>

### useUrlMetadata [@badge=Both]

Returns metadata for a given URL.
[Suspense](/docs/api-reference/liveblocks-react#Suspense-hooks) and
[regular](/docs/api-reference/liveblocks-react#Regular-hooks) versions of this
hook are available.

```tsx
import { useUrlMetadata } from "@liveblocks/react";

const { metadata, error, isLoading } = useUrlMetadata("https://liveblocks.io");

// metadata.title, metadata.description, metadata.image, metadata.icon, ...
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="url" type="string">
    The URL to get metadata for.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
  <PropertiesListItem name="metadata" type="UrlMetadata | undefined">
    The metadata for the URL, or `undefined` if not yet loaded (in non-Suspense
    version).
  </PropertiesListItem>
  <PropertiesListItem name="isLoading" type="boolean">
    Whether the metadata is currently loading.
  </PropertiesListItem>
  <PropertiesListItem name="error" type="Error | undefined">
    Any error that occurred while loading the metadata.
  </PropertiesListItem>
</PropertiesList>

## TypeScript

### Typing your data

It’s possible to have automatic types flow through your application by defining
a global `Liveblocks` interface. We recommend doing this in a
`liveblocks.config.ts` file in the root of your app, so it’s easy to keep track
of your types. Each type (`Presence`, `Storage`, etc.), is optional, but it’s
recommended to make use of them.

```ts file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    // Each user's Presence, for useMyPresence, useOthers, etc.
    Presence: {};

    // The Storage tree for the room, for useMutation, useStorage, etc.
    Storage: {};

    UserMeta: {
      id: string;
      // Custom user info set when authenticating with a secret key
      info: {};
    };

    // Custom events, for useBroadcastEvent, useEventListener
    RoomEvent: {};

    // Custom metadata set on threads, for useThreads, useCreateThread, etc.
    ThreadMetadata: {};

    // Custom room info set with resolveRoomsInfo, for useRoomInfo
    RoomInfo: {};

    // Custom group info set with resolveGroupsInfo, for useGroupInfo
    GroupInfo: {};

    // Custom activities data for custom notification kinds
    ActivitiesData: {};
  }
}

// Necessary if you have no imports/exports
export {};
```

Here are some example values that might be used.

```ts file="liveblocks.config.ts"
import { LiveList } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    // Each user's Presence, for useMyPresence, useOthers, etc.
    Presence: {
      // Example, real-time cursor coordinates
      cursor: { x: number; y: number };
    };

    // The Storage tree for the room, for useMutation, useStorage, etc.
    Storage: {
      // Example, a conflict-free list
      animals: LiveList<string>;
    };

    UserMeta: {
      id: string;
      // Custom user info set when authenticating with a secret key
      info: {
        // Example properties, for useSelf, useUser, useOthers, etc.
        name: string;
        avatar: string;
      };
    };

    // Custom events, for useBroadcastEvent, useEventListener
    // Example has two events, using a union
    RoomEvent: { type: "PLAY" } | { type: "REACTION"; emoji: "🔥" };

    // Custom metadata set on threads, for useThreads, useCreateThread, etc.
    ThreadMetadata: {
      // Example, attaching coordinates to a thread
      x: number;
      y: number;
    };

    // Custom room info set with resolveRoomsInfo, for useRoomInfo
    RoomInfo: {
      // Example, rooms with a title and url
      title: string;
      url: string;
    };

    // Custom group info set with resolveGroupsInfo, for useGroupInfo
    GroupInfo: {
      // Example, groups with a name and a badge
      name: string;
      badge: string;
    };

    // Custom activities data for custom notification kinds
    ActivitiesData: {
      // Example, a custom $alert kind
      $alert: {
        title: string;
        message: string;
      };
    };
  }
}

// Necessary if you have no imports/exports
export {};
```

### Typing with createRoomContext

Before Liveblocks 2.0, it was recommended to create your hooks using
[`createRoomContext`][], and manually pass your types to this function. This is
no longer [the recommended method](#Typing-your-data) for setting up Liveblocks,
but it can still be helpful, for example you can use `createRoomContext`
multiple times to create different room types, each with their own correctly
typed hooks.

```tsx file="liveblocks.config.ts" isCollapsable isCollapsed
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";

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

// Presence represents the properties that exist on every user in the Room
// and that will automatically be kept in sync. Accessible through the
// `user.presence` property. Must be JSON-serializable.
type Presence = {
  // cursor: { x: number, y: number } | null,
  // ...
};

// Optionally, Storage represents the shared document that persists in the
// Room, even after all users leave. Fields under Storage typically are
// LiveList, LiveMap, LiveObject instances, for which updates are
// automatically persisted and synced to all connected clients.
type Storage = {
  // animals: LiveList<string>,
  // ...
};

// Optionally, UserMeta represents static/readonly metadata on each user, as
// provided by your own custom auth back end (if used). Useful for data that
// will not change during a session, like a user’s name or avatar.
// type UserMeta = {
//   id?: string,  // Accessible through `user.id`
//   info?: Json,  // Accessible through `user.info`
// };

// Optionally, the type of custom events broadcast and listened to in this
// room. Use a union for multiple events. Must be JSON-serializable.
// type RoomEvent = {};

// Optionally, when using Comments, ThreadMetadata represents metadata on
// each thread. Can only contain booleans, strings, and numbers.
// export type ThreadMetadata = {
//   pinned: boolean;
//   quote: string;
//   time: number;
// };

export const {
  RoomProvider,
  useMyPresence,
  useStorage,

  // Other hooks
  // ...
} = createRoomContext<
  Presence,
  Storage
  /* UserMeta, RoomEvent, ThreadMetadata */
>(client);
```

To upgrade to Liveblocks 2.0 and the new typing system, follow the
[2.0 migration guide](/docs/platform/upgrading/2.0).

### User [#user-type]

`User` is a type that’s returned by [`useSelf`][], [`useOthers`][], and other
functions. Some of its values are set when
[typing your room](#Typing-your-data), here are some example values:

```ts file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    // Each user’s Presence
    // +++
    Presence: {
      cursor: { x: number; y: number };
    };
    // +++

    UserMeta: {
      id: string;
      // Custom user info set when authenticating with a secret key
      // +++
      info: {
        name: string;
        avatar: string;
      };
      // +++
    };
  }
}
```

```ts
const { room, leave } = client.enterRoom("my-room-id");

// {
//   connectionId: 52,
//   +++
//   presence: {
//     cursor: { x: 263, y: 786 },
//   },
//   +++
//   id: "mislav.abha@example.com",
//   +++
//   info: {
//     name: "Mislav Abha",
//     avatar: "/mislav.png",
//   },
//   +++
//   canWrite: true,
//   canComment: true,
// }
const user = room.getSelf();
```

<PropertiesList title="Properties">
  <PropertiesListItem name="connectionId" type="number">
    The connection ID of the User. It is unique and increments with every new
    connection.
  </PropertiesListItem>
  <PropertiesListItem name="id" type={`UserMeta["id"]`}>
    The ID of the User that has been set in the authentication endpoint. Useful
    to get additional information about the connected user.
  </PropertiesListItem>
  <PropertiesListItem name="info" type={`UserMeta["info"]`}>
    Additional user information that has been set in the authentication
    endpoint.
  </PropertiesListItem>
  <PropertiesListItem name="presence" type={`TPresence`}>
    The user’s Presence data.
  </PropertiesListItem>
  <PropertiesListItem name="canWrite" type="boolean">
    `true` if the user can mutate the Room’s Storage and/or YDoc, `false` if
    they can only read but not mutate it. Set via your [room
    permissions](/docs/authentication#Room-permissions).
  </PropertiesListItem>
  <PropertiesListItem name="canComment" type="boolean">
    `true` if the user can leave a comment in the room, `false` if they can only
    read comments but not leave them. Set via your [room
    permissions](/docs/authentication#Room-permissions).
  </PropertiesListItem>
</PropertiesList>

## Helpers

### shallow

Compares two values shallowly. This can be used as the second argument to
selector based functions to loosen the equality check:

```tsx
const redShapes = useStorage(
  (root) => root.shapes.filter((shape) => shape.color === "red"),
  shallow // 👈 here
);
```

The default way [selector results](#selectors-return-arbitrary-values) are
compared is by checking referential equality (`===`). If your selector returns
computed arrays (like in the example above) or objects, this will not work.

By passing `shallow` as the second argument, you can “loosen” this check. This
is because `shallow` will shallowly compare the members of an array (or values
in an object):

```tsx
// Comparing arrays
shallow([1, 2, 3], [1, 2, 3]); // true

// Comparison objects
shallow({ a: 1 }, { a: 1 }); // true
```

Please note that this will only do a shallow (one level deep) check. Hence the
name. If you need to do an arbitrarily deep equality check, you’ll have to write
a custom equality function or use a library like Lodash for that.

## How selectors work [#selectors] [@keywords=["useStorage", "useSelf", "useOthers", "useOther", "useOthersMapped", "useOthersConnectionIds", "selectors", "comparison"]]

The concepts and behaviors described in this section apply to all of our
selector hooks: [`useStorage`][] , [`useSelf`][] , [`useOthers`][] ,
[`useOthersMapped`][], and [`useOther`][] (singular).

```tsx file="Component.tsx"
const child = useStorage((root) => root.child);
const nested = useStorage((root) => root.child.nested);
const total = useStorage((root) => root.x + root.y);
const merged = useStorage((root) => [...root.items, ...root.more], shallow);
```

<Banner title="Examples are illustrated via useStorage">

In this section, `useStorage` is used as the canonical example. This is for
illustration purposes only. The described concepts and behaviors apply equally
to the other selector hooks.

</Banner>

In a nutshell, the key behaviors for all selector APIs are:

- They [receive immutable data](#selectors-receive-immutable-data)
- They [return arbitrary values](#selectors-return-arbitrary-values)
- They [auto-subscribe to updates](#selectors-subscribe-to-updates)

Let’s go over these traits and responsibilities in the next few sections.

### Selectors receive immutable data [#selectors-receive-immutable-data]

The received input to all selector functions is a **read-only** and
**immutable** top level context value that differs for each hook:

- `useStorage((root) => ...)` receives the Storage root
- `useSelf((me) => ...)` receives the current user
- `useOthers((others) => ...)` receives a list of other users in the room
- `useOthersMapped((other) => ...)` receives each individual other user in the
  room
- `useOther(connectionId, (other) => ...)` receives a specific user in the room

For example, suppose you have set up Storage in the typical way by setting
`initialStorage` in your [`RoomProvider`][] to a tree that describes your app’s
data model using `LiveList`, `LiveObject`, and `LiveMap`. The "root" argument
for your selector function, however, will receive **an immutable and read-only
representation** of that Storage tree, consisting of "normal" JavaScript
datastructures. This makes consumption much easier.

```tsx file="Component.tsx"
function Component() {
  useStorage((root) => ...);
  //          ^^^^
  //          Read-only. No mutable Live structures in here.
  //
  //          {
  //            animals: ["🦁", "🦊", "🐵"],
  //            mathematician: { firstName: "Ada", lastName: "Lovelace" },
  //            fruitsByName: new Map([
  //              ["apple", "🍎"],
  //              ["banana", "🍌"],
  //              ["cherry", "🍒"],
  //            ])
  //          }
  //
}
```

Internally, these read-only trees use a technique called **structural sharing**.
This means that between rerenders, if nodes in the tree did not change, they
will **guarantee** to return the same memory instance. Selecting and returning
these nodes directly is therefore safe and considered a good practice, because
they are stable references by design.

### Selectors return arbitrary values [#selectors-return-arbitrary-values] [@keywords=["shallow"]]

```tsx file="Component.tsx"
const animals = useStorage((root) => root.animals);
// ["🦁", "🦊", "🐵"]

const ada = useStorage((root) => root.mathematician);
// { firstName: "Ada", lastName: "Lovelace" }

const fullname = useStorage(
  (root) => `${root.mathematician.firstName} ${root.mathematician.lastName}`
);
// "Ada Lovelace"

const fruits = useStorage((root) => [...root.fruitsByName.values()], shallow);
// ["🍎", "🍌", "🍒"]
```

Selectors you write can return _any_ value. You can use it to “just” select
nodes from the root tree (first two examples above), but you can also return
computed values, like in the last two examples.

#### Selector functions must return a stable result

One important rule is that selector functions **must return a stable result** to
be efficient. This means calling the same selector twice with the same argument
should return two results that are _referentially equal_. Special care needs to
be taken when filtering or mapping over arrays, or when returning object
literals, because those operations create new array or object instances on every
call (the reason why is detailed
[in the next section](#selectors-subscribe-to-updates)).

#### Examples of stable results

<dl>
  <dt>✅ `(root) => root.animals` is stable</dt>
  <dd>
    Liveblocks guarantees this. All nodes in the Storage tree are stable
    references as long as their contents don’t change.
  </dd>
  <dt>️️⚠️ `(root) => root.animals.map(...)` is not stable</dt>
  <dd>
    Because `.map()` creates a new array instance every time. You’ll need to use
    [`shallow`][] here.
  </dd>
  <dt>✅ `(root) => root.animals.map(...).join(", ")` is stable</dt>
  <dd>
    Because `.join()` ultimately returns a string and all primitive values are
    always stable.
  </dd>
</dl>

#### Use a shallow comparison if the result isn’t stable

If your selector function doesn’t return a stable result, it will lead to an
explosion of unnecessary rerenders. In most cases, you can use a [`shallow`][]
comparison function to loosen the check:

```tsx
import { shallow } from "@liveblocks/react";

// ❌ Bad - many unnecessary rerenders
const uncheckedItems = useStorage((root) =>
  root.todos.filter((item) => !item.done)
);

// ✅ Great
const uncheckedItems = useStorage(
  (root) => root.todos.filter((item) => !item.done),
  shallow // 👈 The fix!
);
```

If your selector function constructs complex objects, then a [`shallow`][]
comparison may not suffice. In those advanced cases, you can provide your own
custom comparison function, or use `_.isEqual` from Lodash.

### Selectors auto-subscribe to updates [#selectors-subscribe-to-updates]

Selectors effectively automatically subscribe your components to updates to the
selected or computed values. This means that your component will **automatically
rerender** when the selected value changes.

Using **multiple selector hooks** within a single React component is perfectly
fine. Each such hook will individually listen for data changes. The component
will rerender if _at least one_ of the hooks requires it. If more than one
selector returns a new value, the component _still only rerenders once_.

Technically, deciding if a rerender is needed works by re-running your selector
function `(root) => root.child` every time something changes inside Liveblocks
storage. Anywhere. That happens often in a busy multiplayer app! The reason why
this is still no problem is that even though `root` will be a different value on
every change, `root.child` will not be if it didn’t change (due to how
Liveblocks internally uses structural sharing).

Only once the returned value is different from the previously returned value,
the component will get rerendered. Otherwise, your component will just remain
idle.

Consider the case:

```tsx
function Component() {
  const animals = useStorage((root) => root.animals);
}
```

And the following timeline:

- First render, `root.animals` initially is `["🦁", "🦊", "🐵"]`.
- Then, something unrelated elsewhere in Storage is changed. In response to the
  change, `root.animals` gets re-evaluated, but it still returns the same
  (unchanged) array instance.
- Since the value didn’t change, no rerender is needed.
- Then, someone removes an animal from the list. In response to the change,
  `root.animals` gets re-evaluated, and now it returns `["🦁", "🦊"]`.
- Because the previous value and this value are different, the component will
  rerender, seeing the updated value.

[`createclient`]: /docs/api-reference/liveblocks-client#createClient
[`createroomcontext`]: /docs/api-reference/liveblocks-react#createRoomContext
[`livelist`]: /docs/api-reference/liveblocks-client#LiveList
[`livemap`]: /docs/api-reference/liveblocks-client#LiveMap
[`liveobject`]: /docs/api-reference/liveblocks-client#LiveObject
[`lostconnectiontimeout`]:
  /docs/api-reference/liveblocks-client#createClientLostConnectionTimeout
[`room.history`]: /docs/api-reference/liveblocks-client#Room.history
[`roomprovider`]: /docs/api-reference/liveblocks-react#RoomProvider
[`liveblocksprovider`]: /docs/api-reference/liveblocks-react#LiveblocksProvider
[`usemutation`]: /docs/api-reference/liveblocks-react#useMutation
[`usestorage`]: /docs/api-reference/liveblocks-react#useStorage
[`useself`]: /docs/api-reference/liveblocks-react#useSelf
[`useothers`]: /docs/api-reference/liveblocks-react#useOthers
[`useothersmapped`]: /docs/api-reference/liveblocks-react#useOthersMapped
[`useothersconnectionids`]:
  /docs/api-reference/liveblocks-react#useOthersConnectionIds
[`useother`]: /docs/api-reference/liveblocks-react#useOther
[`uselostconnectionlistener`]:
  /docs/api-reference/liveblocks-react#useLostConnectionListener
[`clientsidesuspense`]: /docs/api-reference/liveblocks-react#ClientSideSuspsnse
[`usebroadcastevent`]: /docs/api-reference/liveblocks-react#useBroadcastEvent
[`useupdatemypresence`]:
  /docs/api-reference/liveblocks-react#useUpdateMyPresence
[`usethreads`]: /docs/api-reference/liveblocks-react#useThreads
[`useinboxnotifications`]:
  /docs/api-reference/liveblocks-react#useInboxNotifications
[`usemypresence`]: /docs/api-reference/liveblocks-react#useMyPresence
[`usesyncstatus`]: /docs/api-reference/liveblocks-react#useSyncStatus
[`useerrorlistener`]: /docs/api-reference/liveblocks-react#useErrorListener
[`room`]: /docs/api-reference/liveblocks-client#Room
[`shallow`]: /docs/api-reference/liveblocks-react#shallow
[`resolveusers`]: /docs/api-reference/liveblocks-client#resolveUsers
[`resolveroomsinfo`]: /docs/api-reference/liveblocks-client#resolveRoomsInfo
[selector]: /docs/api-reference/liveblocks-react#selectors
[how selectors work]: /docs/api-reference/liveblocks-react#selectors
[suspense version]: /docs/api-reference/liveblocks-react#Suspense
[connection status example]:
  https://liveblocks.io/examples/connection-status/nextjs
[`atob`]: https://developer.mozilla.org/en-US/docs/Web/API/atob
[`base-64`]: https://www.npmjs.com/package/base-64
[`websocket`]: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
[`ws`]: https://www.npmjs.com/package/ws
[`fetch`]: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
[`node-fetch`]: https://npmjs.com/package/node-fetch

---
meta:
  title: "@liveblocks/redux"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/redux package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/redux` provides you with [Redux](https://react-redux.js.org/)
bindings for our realtime collaboration APIs, built on top of WebSockets. Read
our [getting started](/docs/get-started) guides to learn more.

## Enhancer

Enhancer that lets you connect a Redux state to Liveblocks Presence and Storage
features.

```js
import { liveblocksEnhancer } from "@liveblocks/redux";
import { configureStore } from "@reduxjs/toolkit";

const store = configureStore({
  reducer: /* reducer */,
  enhancers: (getDefaultEnhancers) =>
    getDefaultEnhancers().concat(
      liveblocksEnhancer({
      client,
      storageMapping: {},
      presenceMapping: {},
    })
  ),
});
```

<PropertiesList title="Options">
  <PropertiesListItem name="client" type="Client">
    The Liveblocks client instance created with `createClient()`.
  </PropertiesListItem>
  <PropertiesListItem name="presenceMapping" type="PresenceMapping">
    Optional mapping to synchronize Redux state with Liveblocks presence.
  </PropertiesListItem>
  <PropertiesListItem name="storageMapping" type="StorageMapping">
    Optional mapping to synchronize Redux state with Liveblocks storage.
  </PropertiesListItem>
</PropertiesList>

### client [#enhancer-option-client]

See different authentication methods in the [`createClient`][] method.

```js highlight="1,4-6,10-11"
import { createClient } from "@liveblocks/client";
import { liveblocksEnhancer } from "@liveblocks/redux";

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

const store = configureStore({
  reducer: /* reducer */,
  enhancers: (getDefaultEnhancers) =>
    getDefaultEnhancers().concat(liveblocksEnhancer({ client })),
});
```

### presenceMapping [#enhancer-option-presence-mapping]

Mapping used to synchronize a part of your Redux state with one Liveblocks room
presence.

```js highlight="21"
import { liveblocksEnhancer } from "@liveblocks/redux";

const initialState = {
  cursor: { x: 0, y: 0 },
};

const slice = createSlice({
  name: "state",
  initialState,
  reducers: {
    /* reducers */
  },
});

const store = configureStore({
  reducer: slice.reducer,
  enhancers: (getDefaultEnhancers) =>
    getDefaultEnhancers().concat(
      liveblocksEnhancer({
        client,
        presenceMapping: { cursor: true },
      })
    ),
});
```

### storageMapping [#enhancer-option-storage-mapping]

Mapping used to synchronize a part of your Redux state with one Liveblocks Room
storage.

```js highlight="21"
import { liveblocksEnhancer } from "@liveblocks/redux";

const initialState = {
  scientist: { name: "" },
};

const slice = createSlice({
  name: "state",
  initialState,
  reducers: {
    /* reducers */
  },
});

const store = configureStore({
  reducer: slice.reducer,
  enhancers: (getDefaultEnhancers) =>
    getDefaultEnhancers().concat(
      liveblocksEnhancer({
        client,
        storageMapping: { scientist: true },
      })
    ),
});
```

## Actions

### ENTER [#actions-enter]

Dispatch `enterRoom` action to enter a room and start syncing it with Redux
state.

- `roomId`: The room’s ID.

```js
import { actions } from "@liveblocks/redux";
import { useDispatch, useSelector } from "react-redux";

const dispatch = useDispatch();

dispatch(actions.enterRoom("roomId"));
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="roomId" type="string">
    The ID of the room to enter.
  </PropertiesListItem>
</PropertiesList>

### LEAVE [#actions-leave]

Dispatch `leaveRoom` action to leave the current room and stop syncing it with
Redux state.

```js
import { actions } from "@liveblocks/redux";
import { useDispatch, useSelector } from "react-redux";

const dispatch = useDispatch();

dispatch(actions.leaveRoom());
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

## state.liveblocks [#liveblocks-state]

Liveblocks extra state attached by the enhancer.

### others [#liveblocks-state-others]

Other users in the room. Empty when no room is currently synced.

```js
const others = useSelector((state) => state.liveblocks.others);
```

### isStorageLoading [#liveblocks-state-is-storage-loading]

Whether the room storage is currently loading.

```js
const isStorageLoading = useSelector(
  (state) => state.liveblocks.isStorageLoading
);
```

### status [#liveblocks-state-status]

Gets the current WebSocket connection status of the room.

```js
const {
  liveblocks: { status },
} = useStore();
```

The possible values are: `initial`, `connecting`, `connected`, `reconnecting`,
or `disconnected`.

[`createclient`]: /docs/api-reference/liveblocks-client#createClient
[`status`]: /docs/api-reference/liveblocks-redux#liveblocks-state-status

---
meta:
  title: "@liveblocks/yjs"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/yjs package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/yjs` is a [Yjs](https://yjs.dev/) provider enabling you to use
Liveblocks as the hosted back end of your realtime collaborative application.
Read our [getting started](/docs/get-started) guides to learn more.

## Setup

To set up Yjs, it’s recommended to use [`getYjsProviderForRoom`][]. It’s no
longer recommended to use [`LiveblocksYjsProvider`][] directly, as issues may
happen when dynamically switching between rooms.

### React

In React, pass your room object with [`useRoom`][]. It’s fine to use this
function in your React components. From here, you can access your `Y.Doc`.

```ts
import { useRoom } from "@liveblocks/react";
import { getYjsProviderForRoom } from "@liveblocks/yjs";

function App() {
  const room = useRoom();
  const yProvider = getYjsProviderForRoom(room);
  const yDoc = yProvider.getYDoc();

  // ...
}
```

### JavaScript

In JavaScript, pass the room retrieved with [`client.enterRoom`][].

```tsx
import { createClient } from "@liveblocks/client";
import { getYjsProviderForRoom } from "@liveblocks/yjs";

const client = createClient({
  // Options
  // ...
});

const { room, leave } = client.enterRoom("my-room-id", {
  // Options
  // ...
});

const yProvider = getYjsProviderForRoom(room);
const yDoc = yProvider.getYDoc();
```

## getYjsProviderForRoom [#getYjsProviderForRoom]

Gets the current or creates a new [`LiveblocksYjsProvider`][] for a room—this is
the recommended way to use Yjs. This provider will automatically be cleaned up
when the room is destroyed, so you don’t need to destroy the provider manually.
The second argument is the [`LiveblocksYjsProvider`][] options.

```ts
import { getYjsProviderForRoom } from "@liveblocks/yjs";

const yProvider = getYjsProviderForRoom(room, {
  // Options
  // ...
});

const yDoc = yProvider.getYDoc();
```

<PropertiesList title="Returns">
  <PropertiesListItem name="yProvider" type="LiveblocksYjsProvider">
    The [`LiveblocksYjsProvider`][] for the room. The provider is automatically
    cleaned up and destroyed when necessary. Fetch your [`Y.Doc`][] with
    [`LiveblocksYjsProvider.getYDoc`](#LiveblocksYjsProvider.getYDoc).
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Arguments">
  <PropertiesListItem name="room" type="Room" required>
    The Liveblocks room, retrieved with [`useRoom`][] or [`client.enterRoom`][].
  </PropertiesListItem>
  <PropertiesListItem
    name="options.autoloadSubdocs"
    type="boolean"
    defaultValue="false"
  >
    This option will load subdocs automatically.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.enablePermanentUserData"
    type="boolean"
    defaultValue="false"
  >
    This option enables Yjs permanent user data class used by some libraries for
    tracking changes by author.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.offlineSupport_experimental"
    type="boolean"
    defaultValue="false"
  >
    Experimental. Enable offline support using IndexedDB. This means that after
    the first load, documents will be stored locally and load instantly.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.useV2Encoding_experimental"
    type="boolean"
    defaultValue="false"
  >
    Experimental. Use V2 encoding.
  </PropertiesListItem>
</PropertiesList>

## LiveblocksYjsProvider [#LiveblocksYjsProvider]

`LiveblocksYjsProvider` is a
[Yjs provider](https://github.com/yjs/yjs#providers) that allows you to connect
a Yjs document to Liveblocks. Any changes you make to the document will be
stored on Liveblocks servers and synchronized with other clients in the room.

<Banner title="Not recommended to use directly" type="warning">

We generally recommend getting your Liveblocks Yjs provider with
[`getYjsProviderForRoom`][] as it overcomes problems caused when dynamically
switching between rooms.

</Banner>

You can connect by creating a Yjs document, then passing it to
`LiveblocksYjsProvider` along with the currently connected Liveblocks room.

```ts highlight="13-15"
import * as Y from "yjs";
import { createClient } from "@liveblocks/client";
import { LiveblocksYjsProvider } from "@liveblocks/yjs";

const client = createClient({
  publicApiKey: "{{PUBLIC_KEY}}",
});

const { room, leave } = client.enterRoom("your-room-id");

// Create Yjs document and provider
const yDoc = new Y.Doc();
const yProvider = new LiveblocksYjsProvider(room, yDoc, {
  // Options
  // ...
});
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="room" type="Room" required>
    The Liveblocks room, retrieved with [`useRoom`][] or [`client.enterRoom`][].
  </PropertiesListItem>
  <PropertiesListItem name="yDoc" type="Y.Doc" required>
    The [`Y.Doc`][] for the document.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.autoloadSubdocs"
    type="boolean"
    defaultValue="false"
  >
    This option will load subdocs automatically.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.enablePermanentUserData"
    type="boolean"
    defaultValue="false"
  >
    This option enables Yjs permanent user data class used by some libraries for
    tracking changes by author.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.offlineSupport_experimental"
    type="boolean"
    defaultValue="false"
  >
    Experimental. Enable offline support using IndexedDB. This means the after
    the first load, documents will be stored locally and load instantly.
  </PropertiesListItem>
  <PropertiesListItem
    name="options.useV2Encoding_experimental"
    type="boolean"
    defaultValue="false"
  >
    Experimental. Use V2 encoding.
  </PropertiesListItem>
</PropertiesList>

### LiveblocksYjsProvider.getYDoc

Returns the current room’s root [`Y.Doc`][].

```ts
// Root Y.Doc for the room
const yDoc = yProvider.getYDoc();
```

### LiveblocksYjsProvider.awareness [#LiveblocksYjsProvider.awareness]

The [awareness](#Awareness) instance attached to the provider.

```ts
// Yjs awareness
const awareness = yProvider.awareness;
```

### LiveblocksYjsProvider.destroy

Cleanup function. Destroys the [`LiveblocksYjsProvider`][] instance and removes
all resources.

```ts
// Clean up yProvider
yProvider.destroy();
```

### LiveblocksYjsProvider.on("sync") [#LiveblocksYjsProvider.on.sync]

Add an event listener for the `sync` event. The `sync` event is triggered when
the client has received content from the server. Can be used to fire events when
the document has loaded.

```ts
// Listen for the sync event
yProvider.on("sync", (isSynced: boolean) => {
  if (isSynced === true) {
    // Yjs content is synchronized and ready
  } else {
    // Yjs content is not synchronized
  }
});
```

Aliased by `LiveblocksYjsProvider.on("synced")`.

```ts
// "sync" and "synced" both listen to the same event
yProvider.on("sync", (sync: boolean) => /* ... */);
yProvider.on("synced", (sync: boolean) => /* ... */);
```

### LiveblocksYjsProvider.off("sync") [#LiveblocksYjsProvider.off.sync]

Remove an event listener for the `sync` event. The `sync` event is triggered
when the client has received content from the server. Used to clean up
[`LiveblocksYjsProvider.on("sync")`][].

```ts
const handleSync = (synced: boolean) => {};
yProvider.on("sync", handleSync);

// Clean up sync event
yProvider.off("sync", handleSync);
```

Aliased by `LiveblocksYjsProvider.on("synced")`.

```ts
// "sync" and "synced" both listen to the same event
yProvider.off("sync", (sync: boolean) => /* ... */);
yProvider.off("synced", (sync: boolean) => /* ... */);
```

### LiveblocksYjsProvider.once("sync") [#LiveblocksYjsProvider.once.sync]

Add a one-time event listener for the `sync` event. The `sync` event is
triggered when the client has received content from the server. Can be used to
fire events when the document has loaded.

```ts
// Listen for the sync event only once
yProvider.once("sync", (isSynced: boolean) => {
  if (isSynced === true) {
    // Yjs content is synchronized and ready
  } else {
    // Yjs content is not synchronized
  }
});
```

Aliased by `LiveblocksYjsProvider.once("synced")`.

```ts
// "sync" and "synced" both listen to the same event
yProvider.once("sync", (sync: boolean) => /* ... */);
yProvider.once("synced", (sync: boolean) => /* ... */);
```

### LiveblocksYjsProvider.emit("sync") [#LiveblocksYjsProvider.emit.sync]

Synchronously call each listener for the `sync` event in the order they were
registered, passing the supplied arguments to each.

```ts
// Call each listener and pass `true` as an argument
yProvider.emit("sync", true);
```

Aliased by `LiveblocksYjsProvider.emit("synced")`.

```ts
// "sync" and "synced" both listen to the same event
yProvider.emit("sync" /* , ... */);
yProvider.emit("synced" /* , ... */);
```

### LiveblocksYjsProvider.synced [#LiveblocksYjsProvider.synced]

Boolean. Returns whether the client is synchronized with the back end.

```ts
// Check if Yjs content is synchronized with the server
const isSynced: boolean = yProvider.synced;
```

### LiveblocksYjsProvider.connect [#LiveblocksYjsProvider.connect]

Does nothing, added for compatibility. Connections are handled by the
[Liveblocks client](https://liveblocks.io/docs/api-reference/liveblocks-client#createClient).

### LiveblocksYjsProvider.disconnect [#LiveblocksYjsProvider.disconnect]

Does nothing, added for compatibility. Connections are handled by the
[Liveblocks client](https://liveblocks.io/docs/api-reference/liveblocks-client#createClient).

## Awareness [#Awareness]

[`LiveblocksYjsProvider`][] instances have an `awareness` property, which is
powered by
[Liveblocks Presence](/docs/api-reference/liveblocks-client#Room.getPresence).
You can pass it to various bindings which implement awareness, for example
plugins that enable multiplayer cursors in text editors.

```ts
const yDoc = new Y.Doc();
const yProvider = new LiveblocksYjsProvider(room, yDoc);

// Yjs awareness
const awareness = yProvider.awareness;
```

Because awareness is part of presence, it’s also accessible with
[`room.getPresence`][] and [`useMyPresence`][] under the `__yjs` property.

```ts
// Yjs awareness
const awareness = room.getPresence().__yjs;
```

### Awareness.doc [#Awareness.doc]

The Yjs document that the current awareness instance is attached to.

```ts
// The current Yjs document
const yDoc: Y.Doc = awareness.doc;
```

### Awareness.clientId [#Awareness.clientId]

A unique number identifying which client this awareness object is attached to.

```ts
// A unique number representing the current user
const clientId: number = awareness.clientId;
```

### Awareness.getLocalState [#Awareness.getLocalState]

Get the current user’s awareness state.

```ts
// The current user’s awareness
const localState: unknown = awareness.getLocalState();
```

### Awareness.setLocalState [#Awareness.setLocalState]

Set the current user’s awareness state. Accepts JSON-compatible objects.

```ts
// Set the current user’s awareness
awareness.setLocalState({
  user: {
    name: "Jonathan",
  },
});
```

### Awareness.setLocalStateField [#Awareness.setLocalStateField]

Set a single property in the current user’s awareness state. Accepts
JSON-compatible objects, or `null` to remove a property.

```ts
// Set a single property on the current user’s awareness
awareness.setLocalStateField("user", { name: "Jonathan" });
```

### Awareness.getStates [#Awareness.getStates]

Returns a `Map` of states for each client, with each user’s unique `clientId` as
the key.

```ts
// A Map of each user’s awareness state
const states: Map<number, unknown> = awareness.getStates();
```

### Awareness.states [#Awareness.states]

A `Map` of states for each client, with each user’s unique `clientId` as the
key.

```ts
// A Map of each user’s awareness state
const states: Map<number, unknown> = awareness.states;
```

### Awareness.meta [#Awareness.meta]

Provided for compatibility, but generally not necessary. This would be used for
handling user awareness timeouts, but internally awareness uses Liveblocks
Presence, and this handles it for you.

```ts
const meta: Map<number, { click: number; lastUpdated: number }> =
  awareness.meta;
```

### Awareness.destroy [#Awareness.destroy]

Provided for compatibility, but generally not necessary. Cleanup function.
Destroys the [`Awareness`][] instance and removes all resources. Used internally
by [`LiveblocksYjsProvider`][].

```ts
// Cleanup function
awareness.destroy();
```

### Awareness.on("destroyed") [#Awareness.on.destroyed]

Provided for compatibility, but generally not necessary. Add an event listener
for the `destroy` event. The `destroy` event is triggered when
[`awareness.destroy`][] has been called.

```ts
awareness.on("destroyed", () => {
  // Awareness has been cleaned up
});
```

### Awareness.off("destroyed") [#Awareness.off.destroyed]

Provided for compatibility, but generally not necessary. Remove an event
listener for the `destroy` event. The `destroy` event is triggered when
[`awareness.destroy`][] has been called. Used to clean up
[`Awareness.on("destroyed")`.]

```ts
const handleDestroy = () => {};
awareness.on("destroyed", handleDestroy);

// Clean up destroy event
awareness.off("destroyed", handleDestroy);
```

### Awareness.once("destroyed") [#Awareness.once.destroyed]

Provided for compatibility, but generally not necessary. Add a one-time event
listener for the `destroy` event. The `destroy` event is triggered when
[`awareness.destroy`][] has been called.

```ts
awareness.once("destroyed", () => {
  // Awareness has been cleaned up
});
```

### Awareness.emit("destroyed") [#Awareness.emit.destroyed]

Synchronously call each listener for the `destroy` event in the order they were
registered, passing the supplied arguments to each.

```ts
// Call each listener and pass `true` as an argument
awareness.emit("destroy", true);
```

[`@liveblocks/react`]: /docs/api-reference/liveblocks-react
[`useRoom`]: /docs/api-reference/liveblocks-react#useRoom
[`room.getPresence`]: /docs/api-reference/liveblocks-client#Room.getPresence
[`client.enterRoom`]: /docs/api-reference/liveblocks-client#Client.enterRoom
[`useMyPresence`]: /docs/api-reference/liveblocks-react#useMyPresence
[`getYjsProviderForRoom`]: #getYjsProviderForRoom
[`LiveblocksYjsProvider`]: #LiveblocksYjsProvider
[`LiveblocksYjsProvider.awareness]: #LiveblocksYjsProvider.awareness
[`LiveblocksYjsProvider.destroy]: #LiveblocksYjsProvider.destroy
[`LiveblocksYjsProvider.on("sync")`]: #LiveblocksYjsProvider.on.sync
[`LiveblocksYjsProvider.off("sync")`]: #LiveblocksYjsProvider.off.sync
[`LiveblocksYjsProvider.once("sync")`]: #LiveblocksYjsProvider.once.sync
[`LiveblocksYjsProvider.emit("sync")`]: #LiveblocksYjsProvider.emit.sync
[`LiveblocksYjsProvider.synced`]: #LiveblocksYjsProvider.synced
[`LiveblocksYjsProvider.connect`]: #LiveblocksYjsProvider.connect
[`LiveblocksYjsProvider.disconnect`]: #LiveblocksYjsProvider.disconnect
[`Awareness`]: #Awareness
[`Awareness.doc`]: #Awareness.doc
[`Awareness.clientId]: #Awareness.clientId
[`Awareness.getLocalState`]: #Awareness.getLocalState
[`Awareness.setLocalState`]: #Awareness.setLocalState
[`Awareness.setLocalStateField`]: #Awareness.setLocalStateField
[`Awareness.getStates`]: #Awareness.getStates
[`Awareness.states`]: #Awareness.states
[`Awareness.destroy`]: #Awareness.destroy
[`Awareness.meta`]: #Awareness.meta
[`Awareness.on("destroyed")`]: #Awareness.on.destroyed
[`Awareness.off("destroyed")`]: #Awareness.off.destroyed
[`Awareness.once("destroyed")`]: #Awareness.once.destroyed
[`Awareness.emit("destroyed")`]: #Awareness.emit.destroyed
[`Y.Doc`]: https://docs.yjs.dev/api/y.doc

---
meta:
  title: "@liveblocks/zustand"
  parentTitle: "API Reference"
  description: "API Reference for the @liveblocks/zustand package"
alwaysShowAllNavigationLevels: false
---

`@liveblocks/zustand` provides you with [Zustand](https://docs.pmnd.rs/zustand)
bindings for our realtime collaboration APIs, built on top of WebSockets. Read
our [getting started](/docs/get-started) guides to learn more.

## Middleware

The `liveblocks` middleware lets you connect a Zustand state to Liveblocks
Presence and Storage features.

```js highlight="2,5,9-13"
import create from "zustand";
import { liveblocks } from "@liveblocks/zustand";

const useStore = create(
  liveblocks(
    (set) => ({
      /* state and actions */
    }),
    {
      client,
      presenceMapping: {},
      storageMapping: {},
    }
  )
);
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="stateCreator" type="StateCreator">
    The Zustand state creator function.
  </PropertiesListItem>
  <PropertiesListItem name="options" type="LiveblocksMiddlewareOptions">
    Configuration object for the Liveblocks middleware.
  </PropertiesListItem>
</PropertiesList>

<PropertiesList title="Options">
  <PropertiesListItem name="client" type="Client">
    The Liveblocks client instance created with `createClient()`.
  </PropertiesListItem>
  <PropertiesListItem name="presenceMapping" type="PresenceMapping">
    Optional mapping to synchronize Zustand state with Liveblocks presence.
  </PropertiesListItem>
  <PropertiesListItem name="storageMapping" type="StorageMapping">
    Optional mapping to synchronize Zustand state with Liveblocks storage.
  </PropertiesListItem>
</PropertiesList>

### client [#middleware-option-client]

See different authentication methods in the [`createClient`][] method.

```js
import { createClient } from "@liveblocks/client";
import { liveblocks } from "@liveblocks/zustand";

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

liveblocks(/* Zustand config */, { client })
```

### presenceMapping [#middleware-option-presence-mapping]

Mapping used to synchronize a part of your Zustand state with one Liveblocks
Room presence.

```js highlight="8"
const useStore = create(
  liveblocks(
    (set) => ({
      cursor: { x: 0, y: 0 },
    }),
    {
      client,
      presenceMapping: { cursor: true },
    }
  )
);
```

### storageMapping [#middleware-option-storage-mapping]

Mapping used to synchronize a part of your Zustand state with one Liveblocks
room storage.

```js highlight="8"
const useStore = create(
  liveblocks(
    (set) => ({
      scientist: { name: "" },
    }),
    {
      client,
      storageMapping: { scientist: true },
    }
  )
);
```

## state.liveblocks [#liveblocks-state]

Liveblocks extra state attached by the liveblocks.

### enterRoom [#liveblocks-state-enter-room]

Enters a room and starts syncing it with your Zustand state.

- `roomId`: The room’s ID.

```js
const {
  liveblocks: { enterRoom },
} = useStore();

enterRoom("roomId");
```

<PropertiesList title="Arguments">
  <PropertiesListItem name="roomId" type="string">
    The ID of the room to enter.
  </PropertiesListItem>
</PropertiesList>

If this is the first time you're entering the room, the room is initialized from
your local Zustand state (only for the keys mentioned in your `storageMapping`
configuration).

### leaveRoom [#liveblocks-state-leave-room]

Leaves the current room and stops syncing it with Zustand state.

```js
const {
  liveblocks: { leaveRoom },
} = useStore();

leaveRoom();
```

<PropertiesListEmpty title="Arguments">_None_</PropertiesListEmpty>

### room [#liveblocks-state-room]

The [`Room`][] currently synced to your Zustand state.

```js
const {
  liveblocks: { room },
} = useStore();
```

### others [#liveblocks-state-others]

Other users in the room. Empty when no room is currently synced.

```js
const {
  liveblocks: { others },
} = useStore();
```

### isStorageLoading [#liveblocks-state-is-storage-loading]

Whether or not the room storage is currently loading.

```js
const {
  liveblocks: { isStorageLoading },
} = useStore();
```

### status [#liveblocks-state-status]

Gets the current WebSocket connection status of the room.

```js
const {
  liveblocks: { status },
} = useStore();
```

The possible values are: `initial`, `connecting`, `connected`, `reconnecting`,
or `disconnected`.

[`createclient`]: /docs/api-reference/liveblocks-client#createClient
[`room`]: /docs/api-reference/liveblocks-client#room
[`status`]: /docs/api-reference/liveblocks-zustand#liveblocks-state-status

---
meta:
  title: "Troubleshooting"
  parentTitle: "Platform"
  description: "Troubleshoot common errors"
---

## Common issues [#common]

<Banner title="Need help troubleshooting?">

Try the [Liveblocks DevTools extension](/devtools) to visualize your
collaborative experiences as you build them, in realtime.

</Banner>

### ReferenceError: process is not defined [#process-not-defined]

When calling `client.enterRoom()`, you stumble upon the following error:

```text
ReferenceError: process is not defined
```

The `@liveblocks/client` package expects to be consumed by a JavaScript bundler,
like Webpack, Babel, ESbuild, Rollup, etc. If you see this error, you have most
likely directly loaded the `@liveblocks/client` source code through a `<script>`
tag.

#### Solution [#process-not-defined-solution] [@hidden]

If using a bundler isn’t possible for you, there are two available solutions.

One solution is to load the source code from the
[Skypack CDN](https://www.skypack.dev/view/@liveblocks/client), which will
expose optimized production builds which have the `process` variable removed.

Another solution is to define the necessary `process.env.NODE_ENV` variable
manually, before loading the script, for example:

```ts
globalThis.process = { env: { NODE_ENV: "production" } };
```

### Why we’re asking you to pass unstable_batchedUpdates [#stale-props-zombie-child]

<Banner title="The best solution is to upgrade to React 18">

The last version of Liveblocks that supports the old React 17 version is 2.14.0.
To upgrade Liveblocks to 2.15 or higher, first upgrade React to 18.

Passing in `unstable_batchedUpdates` is no longer necessary on the latest
version of Liveblocks.

</Banner>

If you’re **on React 17 or lower**, there is a well-known problem that all state
management libraries need to deal with at some point, known as the
["Stale-props / zombie-child" problem](https://kaihao.dev/posts/Stale-props-and-zombie-children-in-Redux)—Liveblocks
is no exception.

This issue can cause catastrophic bugs, inconsistent renders, or—in the best
case—performance issues. Sooner or later, as your app grows in complexity, you
will run into some manifestation of this bug, and we want to be ahead of that.

Just do either of the following to avoid it!

1. **Just upgrade to React 18** (recommended)
1. If you cannot upgrade just yet, we ask you to pass the
   `unstable_batchedUpdates` function to the RoomProvider in the mean time, so
   Liveblocks can circumvent the issue on your behalf!

```tsx highlight="1,7"
import { unstable_batchedUpdates } from "react-dom";
//                                      ^^^^^^^^^^^ ...or "react-native"!
<RoomProvider
  id="my-room"
  initialPresence={/* ... */}
  initialStorage={/* ... */}
  unstable_batchedUpdates={unstable_batchedUpdates}
>
  <App />
</RoomProvider>;
```

Starting with 0.18.3, this will be enforced in React 17 and lower, potentially
saving hours of debugging.

#### What’s the problem, exactly? [#stale-props-zombie-child-details] [@hidden]

If you’re on React 17 or lower, state updates from “external” (non-DOM) events
will **not** get batched together automatically. If two or more of your
components subscribe to the same piece of state, and this state gets updated by
another user in the room, then both components will individually rerender,
separately.

In most cases this is just inefficient but not catastrophic. However, if you
happen to have a parent and child component that both rely on the same state,
this can lead to hard-to-debug bugs.

For example:

```tsx highlight="2,7"
function Parent() {
  const shapes = useStorage((root) => root.shapes);
  return shapes.map(({ id }) => <Child key={id} id={id} />);
}

function Child({ id }) {
  const shape = useStorage((root) => root.shapes.get(id));
  return <Shape x={shape.x} y={shape.y} />;
}
```

Both of these components need to get rerendered if, for example, some shape gets
deleted by another user in the room.

In React 17 (or lower), those rerenders will **not get batched together** and as
such they will not rerender together. Instead, they rerender individually,
separately. Which component rerenders first is undefined and often unpredictable
in larger apps. If you’re unlucky, this can lead to the Child component to get
rerendered before its Parent has had the opportunity to unmount it, which should
of course never happen.

By providing `unstable_batchedUpdates` to the RoomProvider, Liveblocks will wrap
all state updates in this helper, which will make sure that both Parent and
Child get rerendered as part of the same render cycle. This way, React will
ensure that the Parent component will always get rerendered before the Child.

## TypeScript issues [#typescript]

### Type "MyInterface" does not satisfy the constraint "Lson" [#lson-constraint-and-interfaces] [@keywords=["TS2344", "Lson", "LsonObject", "index signature missing"]]

If you found this page, chances are you stumbled upon this TypeScript error:

```text
TS2344: Type 'MyInterface' does not satisfy the constraint 'Lson'.
  Type 'MyInterface' is not assignable to type 'JsonObject'.
    Index signature for type 'string' is missing in type 'MyInterface'.
```

Liveblocks data structures (like `LiveObject`, `LiveMap`, and `LiveList`)
require that their payloads are always JSON-serializable to be able to send them
over WebSocket connections reliably and without surprises. Starting with 0.16,
we’re enforcing this with `Lson` type constraint. (LSON is a Liveblocks-specific
extension of JSON that also allows nesting more Live data structures.)

If you encounter this error above, TypeScript is trying to tell you that the
data type you are using in one of your Live structures is not (guaranteed to be)
a legal LSON (or JSON) value. But why?

```ts
interface Person {
  name: string;
  age: number;
}

const people = new LiveList<Person>();
//                          ~~~~~~
//                          TS2344: Type "Person" does not
//                          satisfy the constraint "Lson" ☹️
```

Although this `Person` type seems perfectly JSON-serializable with only those
`string` and `number` fields, TypeScript still considers this a problem because
it cannot _guarantee_ that all of its subtypes will also be that. Interface
types are "open" and extensible by design. This means it’s possible to define a
subtype that would still not be JSON-serializable.
[Example](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgArQM4HsTIN4BQyyIcAthAFzIZhSgDmA3EcnA1SQK5kBG0LAL4ECAelHIAanAA2XCBmRYYyAAYBRAG7AZ6KNhCrkZYAwAWYElkv9kAKQDKAeQByAWgzRgs4AC84vDIQADRiEgCeEJZwUCi0OjLImj4AJmp6BqoYBKCQsIgoWjoZOMgQAB6QICmKJbiExOLIgLwbgBB7rCDWDl4+-oGcXCAA1p0A7iBMjRIARIMjWOPTyDDAEDI1xqYWVjZxPTJ+AUEEwkA).

To fix this issue, there are roughly three available solutions.

#### Solution 1: Change your interface to a type [#lson-interface-solution1] [@hidden]

The simplest solution is to convert your `interface` to a `type`.

```ts highlight="1"
type Person = {
  name: string;
  age: number;
};

const people = new LiveList<Person>();
//                          ^^^^^^ ✅ All good now
```

Check this solution out in the TypeScript
[playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgGWANwKaoM7wL5wBmUEIcA5AAIA26GARtRAMYDW2A9M7RgHYzkA3AChhMAJ5gMcAAoYo2CLzgBeRMLhxeAQxAYAXHFxRgvAOYjN2swa0BXEPXki8I4cyW44UiGGrS1XgwAdxQ6HBgAHjkFJQA+AAoAShEODk0MzKzs7IA9fIK4QFByOABBamo4MwgIABMtCGDhIA).

#### Solution 2: Extend your interface [#lson-interface-solution2] [@hidden]

You can also explicitly pledge that your interface will be JSON serializable by
having it extend from `JsonObject`.

```ts highlight="1,3"
import { JsonObject } from "@liveblocks/client";

interface Person extends JsonObject {
  name: string;
  age: number;
}

const people = new LiveList<Person>();
//                          ^^^^^^ ✅ All good now
```

Check this solution out in the TypeScript
[playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgGWANwKaoM4wDRwBS2EAdgPIBGAVhgMbwC+cAZlBCHAOQACANugyU+EOgGtsAejoCMpGFwDcAKGXB5GKCwCGdDHAAKmkqTgYAHjDkATbERNVaDRMrhxS2kBgBccXFHUAcxU3bUCfdwBXEEpNFUYVZToyXDgwDAgwPn0AXncMAHcUQRwYAB4jKBMAPgAKAEoVSUk3Vrb2jo6APR7euEBQcjgAQT4+OECICGt3CALlIA).

#### Solution 3: Wrap the interface in a helper [#lson-interface-solution3] [@hidden]

This is the least preferred solution, but may be necessary if you don’t own the
interface definition and it’s coming from an external package.

```ts highlight="1,4"
import type { LiveList, EnsureJson } from "@liveblocks/client";
import { Person } from "some-external-package";

const people = new LiveList<EnsureJson<Person>>();
//                          ^^^^^^^^^^^^^^^^^^ ✅ All good now
```

Check this solution out in the TypeScript
[playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgGWANwKaoM4wDRwCiAdtgK5QYBS2ExcAvnAGZQQhwDkAAgDboYARrwgBjANbYA9KP4ZiMTgG4AUKEixEcACoARAKrYMURizYceMXgBMoAQwDuUq7cfKVUqXG8+4APQDAuAAlO2JrdjhgBWNmO1EMOBgATzBE1kj7cMiMAA8YY2I7XjgwePE7AHMMFRVROlw4MiMobDgAXjhiDAcUARwYAB4SckoaOkG9Q2MAPhmACgBKVSA).

---
meta:
  title: "Authentication"
  description: "Authenticate your users in your application with ID tokens."
---

For production applications, we recommend using your secret API key to
authenticate users with **ID tokens**. Your public API key is only for
prototyping and public applications.

## Quickstart

<ListGrid columns={3}>
  <DocsCard
    title="Next.js"
    href="/docs/authentication/id-token/nextjs"
    visual={<DocsNextjsIcon />}
  />
  <DocsCard
    title="Remix"
    href="/docs/authentication/id-token/remix"
    visual={<DocsRemixIcon />}
  />
  <DocsCard
    title="SvelteKit"
    href="/docs/authentication/id-token/sveltekit"
    visual={<DocsSvelteIcon />}
  />
  <DocsCard
    title="Nuxt.js"
    href="/docs/authentication/id-token/nuxtjs"
    visual={<DocsNuxtjsIcon />}
  />
  <DocsCard
    title="Express"
    href="/docs/authentication/id-token/express"
    visual={<DocsExpressIcon />}
  />
  <DocsCard
    title="Firebase"
    href="/docs/authentication/id-token/firebase"
    visual={<DocsFirebaseIcon />}
  />
</ListGrid>

---

## How ID token authentication works

ID token authentication allows Liveblocks to handle permissions for you. This
means that when you create or modify a room, you can set a user’s permissions on
the room itself. This means the room acts as a source of truth. Later, when a
user tries to enter a room, Liveblocks will automatically check if the user has
permission, and deny them access if the permissions aren’t set.

Permissions aren’t just for individual users, but can also be set for groups of
users, or for the whole room at once.

<Figure>
  <Image
    src="/assets/authentication/id-token-enter-room.png"
    alt="An ID token granting entry to a room"
    width={1494}
    height={840}
    quality={100}
  />
</Figure>

<Banner title="You prefer to handle permissions yourself?">

If you don’t need fine-grained permissions, or if you’d prefer storing
individual room permissions in your own system, you can use
[access tokens](/docs/authentication/access-token) instead.

</Banner>

## Authenticating

Authenticating with ID tokens means creating a
[JSON Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) (JWT) that’s used
to verify the identity of the current user when connecting to a Liveblocks room.
This token is created using
[`liveblocks.identifyUser`](/docs/api-reference/liveblocks-node#id-tokens) or
[`/identify-user`](/docs/api-reference/rest-api-endpoints#post-identify-user).

```ts
const { body, status } = await liveblocks.identifyUser({
  userId: "olivier@example.com",
});

// '{ token: "eyJga7..." }'
console.log(body);
```

## Workspace permissions [#permissions]

Using [tenants](/docs/authentication/tenants), you can create workspaces in your
application, compartmentalizing all resources such as inbox notifications and
rooms. This includes everything associated with rooms such as comment threads,
realtime data stored, and more. This allows you to add a workspace switcher to
your application, separating each of your customers/organizations.

<Figure>
  <Image
    src="/assets/authentication/org-switcher.png"
    alt="A workspace switcher"
    width={1600}
    height={1000}
    quality={90}
  />
</Figure>

### Set up workspace permissions

To set up workspace permissions, pass a `tenantId` when authenticating a user,
ensuring that the user will only have access to resources within this workspace.

```ts
const { body, status } = await liveblocks.identifyUser({
  userId: "olivier@example.com",
  // +++
  tenantId: "my-tenant-id",
  // +++
});

// '{ token: "eyJga7..." }'
console.log(body);
```

When creating a resource on the server, such as a room, pass the `tenantId` to
the resource, to allow the user access.

```ts
const room = await liveblocks.createRoom("my-room-id", {
  defaultAccesses: ["room:write"],
  // +++
  tenantId: "my-tenant-id",
  // +++
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

## Room permissions

ID token authentication allows you to set different permission types on rooms,
assigned at three different levels: default, groups, and users. The system is
flexible enough to enable you to build a permission system that’s helpful for
building invite dialogs, private rooms, and more.

<Figure>
  <img
    src="/assets/managing-rooms/rooms-share-dialog.png"
    alt="Share dialog illustration"
  />
</Figure>

To set room permissions, you can
[create](/docs/api-reference/liveblocks-node#post-rooms) or
[update](/docs/api-reference/liveblocks-node#post-rooms-roomId) a room, passing
permission information in the options.

```ts
const room = await liveblocks.createRoom("a32wQXid4A9", {
  // This is a private room
  defaultAccesses: [],

  // But Olivier can enter
  usersAccesses: {
    "olivier@example.com": ["room:read"],
  },
});
```

### Permission types [#permission-types]

There are three permission values that you can set on rooms.

<dl>
  <dt>`["room:write"]`</dt>
  <dd>
    Full access. Enables people to view and edit the room. On the client,
    [`canWrite`](/docs/api-reference/liveblocks-react#Checking-user-permissions)
    is `true`.
  </dd>
  <dt>`["room:read", "room:presence:write"]`</dt>
  <dd>
    Read access with presence. Enables people to edit their presence, but only
    view the room&rsquo;s storage. On the client,
    [`canWrite`](/docs/api-reference/liveblocks-react#Checking-user-permissions)
    is `false`.
  </dd>
  <dt>`[]`</dt>
  <dd>Private. No one can enter the room.</dd>
</dl>

### Permission levels [#permission-types]

Permission types can be applied at three different levels, enabling complex
entry systems.

<dl>
  <dt>defaultAccesses</dt>
  <dd>The default permission types to apply to the entire room.</dd>
  <dt>groupsAccesses</dt>
  <dd>Permission types to apply to specific groups of users.</dd>
  <dt>usersAccesses</dt>
  <dd>Permission types to apply to specific users.</dd>
</dl>

Each level further down will override access levels defined above, for example a
room with private access will allow a user with `room:write` access to enter.

### Default room permissions

The `defaultAccesses` level is used to set the default permissions of the entire
room.

<Figure>
  <Image
    src="/assets/managing-rooms/rooms-access-denied.png"
    alt="Access denied illustration"
    width={820}
    height={412}
  />
</Figure>

When used in our APIs, this property takes an array, with an empty array `[]`
signifying no access. Add permission types to this array to define the default
access level to your room.

```ts
// Private - no one has access by default
"defaultAccesses": []

// Public - everyone can edit and view the room
"defaultAccesses": ["room:write"]

// Read-only - everyone can view the room, but only presence can be edited
"defaultAccesses": ["room:read", "room:presence:write"]
```

#### Setting room access

We can use the
[`liveblocks.createRoom`](/docs/api-reference/rest-api-endpoints#post-rooms) to
create a new room with public access levels:

```ts highlight="2"
const room = await liveblocks.createRoom("a32wQXid4A9", {
  defaultAccesses: ["room:write"],
});
```

The default permission types can later be modified with
[`liveblocks.updateRoom`](/docs/api-reference/liveblocks-node#post-rooms-roomId),
in this example turning the room private:

```ts highlight="2"
const room = await liveblocks.updateRoom("a32wQXid4A9", {
  defaultAccesses: [],
});
```

### Groups permissions

The `groupsAccesses` level is used to set the default permissions of any given
group within room.

<Figure>
  <video autoPlay loop muted playsInline>
    <source src="/assets/managing-rooms/rooms-groups.mp4" type="video/mp4" />
  </video>
</Figure>

Groups are represented by a `groupId`—a custom string that represents a
selection of users in your app. Groups can be attached to a user by passing an
array of `groupId` values in `groupIds`, during authentication.

```js highlight="10"
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST(request: Request) {
  const { status, body } = await liveblocks.identifyUser({
    userId: "marie@example.com",
    groupIds: ["engineering"],
  });

  return new Response(body, { status });
}
```

In our APIs you can then set group accesses by using the `groupId` as the key,
and an array of permissions as the value.

```ts
// "engineering" group has access to view and edit
"groupsAccesses": {
  "engineering": ["room:write"],
}
```

#### Modifying group access [#permissions-levels-groups-accesses-example]

To allow an “engineering” group access to view a room, and modify their
presence, we can use
[`liveblocks.updateRoom`](/docs/api-reference/liveblocks-node#post-rooms-roomId)
with `engineering` as a `groupId`:

```ts highlight="3"
const room = await liveblocks.updateRoom("a32wQXid4A9", {
  groupsAccesses: {
    engineering: ["room:read", "room:presence:write"],
  },
});
```

After calling this, every user in the “engineering” group will have read-only
access. To remove a group’s permissions, we can use
[`liveblocks.updateRoom`](/docs/api-reference/liveblocks-node#post-rooms-roomId)
again, and set the permission type to `null`:

```ts highlight="7"
const room = await liveblocks.updateRoom("a32wQXid4A9", {
  groupsAccesses: {
    engineering: null,
  },
});
```

### User permissions

The `usersAccesses` level is used to set permissions of any given user within a
room.

<Figure>
  <Image
    src="/assets/managing-rooms/rooms-share-dialog.png"
    alt="Share dialog illustration"
    width={820}
    height={412}
  />
</Figure>

To use this, first a user is given a `userId` during authentication.

```js highlight="9"
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST(request: Request) {
  const { status, body } = await liveblocks.identifyUser({
    userId: "ellen@acme.inc",
  });

  return new Response(body, { status });
}
```

Then, if you want the user with the `userId` id to make edits, set `userId` to
`["room:write"]` within `usersAccesses` when creating or updating a room.

```ts
// user with userId "ellen@acme.inc" has access to view and edit
"usersAccesses": {
  "ellen@acme.inc": ["room:write"]
}
```

#### Checking user access [#permissions-levels-users-accesses-example]

To give them room permission, we can use
[`liveblocks.updateRoom`](/docs/api-reference/liveblocks-node#post-rooms-roomId),
setting write access on their `userId`:

```ts highlight="3"
const room = await liveblocks.updateRoom("a32wQXid4A9", {
  usersAccesses: {
    "ellen@acme.inc": ["room:write"],
  },
});
```

To check a user’s assigned permission types for this room, we can then use
[`liveblocks.updateRoom`](/docs/api-reference/liveblocks-node#get-rooms-roomId)
and check `usersAccesses`:

```ts
const room = await liveblocks.getRoom("a32wQXid4A9");

// { "ellen@acme.inc": ["room:write"] }
console.log(room.data.usersAccesses);
```

## Select your framework [#select-your-framework]

Select your framework for specific instructions on setting up ID token
authentication.

<ListGrid columns={3}>
  <DocsCard
    title="Next.js"
    href="/docs/authentication/id-token/nextjs"
    visual={<DocsNextjsIcon />}
  />
  <DocsCard
    title="Remix"
    href="/docs/authentication/id-token/remix"
    visual={<DocsRemixIcon />}
  />
  <DocsCard
    title="SvelteKit"
    href="/docs/authentication/id-token/sveltekit"
    visual={<DocsSvelteIcon />}
  />
  <DocsCard
    title="Nuxt.js"
    href="/docs/authentication/id-token/nuxtjs"
    visual={<DocsNuxtjsIcon />}
  />
  <DocsCard
    title="Express"
    href="/docs/authentication/id-token/express"
    visual={<DocsExpressIcon />}
  />
  <DocsCard
    title="Firebase"
    href="/docs/authentication/id-token/firebase"
    visual={<DocsFirebaseIcon />}
  />
</ListGrid>

---
meta:
  title: "Authenticate users with access tokens"
  parentTitle: "Authentication"
  description: "Authenticate your users in your application with access tokens."
---

Access token authentication is an _alternative_ method to authenticate your
users in your application. With access tokens, when a user authenticates, it’s
up to you to let Liveblocks know which rooms they should be allowed inside. This
means that you need to manually keep track of which users should be allowed in
which rooms, and apply these permissions yourself each time a user connects.

<Banner title="We recommend ID tokens over access tokens" type="warning">

Access tokens have [limitations when granting nested permissions](#limitations).
If you’re looking to build an application with permissions at organization,
group, and user levels, we recommend using [ID tokens](/docs/authentication)
instead.

</Banner>

<Figure>
  <Image
    src="/assets/authentication/access-token-enter-room.png"
    alt="An access token granting entry to a room"
    width={1494}
    height={840}
    quality={100}
  />
</Figure>

## Authenticating

Authenticating with access tokens means creating a
[JSON Web Token](https://en.wikipedia.org/wiki/JSON_Web_Token) (JWT) that grants
the current user permission to enter certain rooms when connecting to
Liveblocks. An access token is created by calling
[`liveblocks.prepareSession`](/docs/api-reference/liveblocks-node#access-tokens)
then by allowing access to certain rooms.

```ts
const session = liveblocks.prepareSession("olivier@example.com");

// Giving write access to one room, then read access to multiple rooms with a wildcard
session.allow("design:9Hdu73", session.FULL_ACCESS);
session.allow("product:*", session.READ_ACCESS);

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

// '{ token: "j6Fga7..." }'
console.log(body);
```

**Before using access tokens, it’s recommended to read through this entire
page**, as it explains helpful practices for granting access to rooms. However,
if you’d like to get set up now, you can select your framework and read more
later.

<ListGrid columns={3}>
  <DocsCard
    title="Next.js"
    href="/docs/authentication/access-token/nextjs"
    visual={<DocsNextjsIcon />}
  />
  <DocsCard
    title="Remix"
    href="/docs/authentication/access-token/remix"
    visual={<DocsRemixIcon />}
  />
  <DocsCard
    title="SvelteKit"
    href="/docs/authentication/access-token/sveltekit"
    visual={<DocsSvelteIcon />}
  />
  <DocsCard
    title="Nuxt.js"
    href="/docs/authentication/access-token/nuxtjs"
    visual={<DocsNuxtjsIcon />}
  />
  <DocsCard
    title="Express"
    href="/docs/authentication/access-token/express"
    visual={<DocsExpressIcon />}
  />
  <DocsCard
    title="Firebase"
    href="/docs/authentication/access-token/firebase"
    visual={<DocsFirebaseIcon />}
  />
</ListGrid>

## Workspace permissions [#permissions]

Using [tenants](/docs/authentication/tenants), you can create workspaces in your
application, compartmentalizing all resources such as inbox notifications and
rooms. This includes everything associated with rooms such as comment threads,
realtime data stored, and more. This allows you to add a workspace switcher to
your application, separating each of your customers/organizations.

<Figure>
  <Image
    src="/assets/authentication/org-switcher.png"
    alt="A workspace switcher"
    width={1600}
    height={1000}
    quality={90}
  />
</Figure>

### Set up workspace permissions

To set up workspace permissions, pass a `tenantId` when authenticating a user,
ensuring that the user will only have access to resources within this workspace.

```ts
const session = liveblocks.prepareSession("olivier@example.com", {
  // +++
  tenantId: "acme",
  // +++
});
```

When creating a resource on the server, such as a room, pass the `tenantId` to
the resource, to allow the user access.

```ts
const room = await liveblocks.createRoom("my-room-id", {
  defaultAccesses: ["room:write"],
  // +++
  tenantId: "acme",
  // +++
});

// { type: "room", id: "my-room-id", metadata: {...}, ... }
console.log(room);
```

## Room permissions

When granting permissions using access tokens, it’s recommended to use a naming
pattern for your room IDs. This makes it easy to use wildcard permissions,
allowing you to authenticate access to multiple rooms at once. One scenario
where this is helpful, is when rooms and users in your app are part of a team or
group, and you need to permit users entry to each room that’s part of this.

### Group hierarchy

Let’s picture a tenant in your product, Acme, set up using
[workspace permissions](#permissions). This customer has a number of group, and
each group contains a number of documents.

<Figure>
  <Image
    src="/assets/authentication/room-organizations.png"
    srcDark="/assets/authentication/room-organizations-dark.png"
    alt="An organization with documents in different teams"
    width={1536}
    height={864}
    quality={100}
  />
</Figure>

In your application, each group and document has a unique ID, and we can use
these to create a naming pattern for your rooms. For example, in the diagram
above, the Acme organization has a Product group (`product`) with two documents
inside (`6Dsw12`, `L2hr8p`).

### Naming pattern

An example of a naming pattern would be to combine these IDs into a unique room
ID separating them with symbols, such as `<group_id>:<document_id>`. A room ID
following this pattern may look like `product:6Dsw1z`.

<Figure>
  <Image
    src="/assets/authentication/room-name-pattern.png"
    srcDark="/assets/authentication/room-name-pattern-dark.png"
    alt="Splitting a room ID into the pattern detailed above"
    width={1536}
    height={864}
    quality={100}
  />
</Figure>

<Banner title="Example pattern">

This example is not a strict naming pattern you must follow, and you can use any
pattern you like. Take care to avoid using your separator character in any other
part of the room ID.

</Banner>

### Wildcard permissions

Assuming you’re using the naming pattern displayed above, you can then grant
access to multiple rooms at once using wildcards.

<Figure>
  <Image
    src="/assets/authentication/access-token-room-naming.png"
    alt="An access token using a wildcard to access multiple rooms"
    width={1494}
    height={840}
    quality={100}
  />
</Figure>

In the image above, you can see that _Olivier_ has access to multiple _product_
rooms, thanks to the `product:*` wildcard rule. This is how he was authorized:

```ts
const session = liveblocks.prepareSession("olivier@example.com");

// Giving full access to one room
session.allow("design:9Hdu73", session.FULL_ACCESS);

// Give full access to every room with an ID beginning with "product:"
session.allow("product:*", session.FULL_ACCESS);

const { body, status } = await session.authorize();
```

Note that you can only use a wildcard at the end of a room ID.

```jsx
// ❌ Wildcard must be at the end of the room ID
session.allow("*:product", session.FULL_ACCESS);

// ✅ Valid wildcard
session.allow("product:*", session.FULL_ACCESS);
```

### Read-only access

Should we wish to grant read-only access to each room, we then add another line
to enable this.

```ts highlight="9-10"
const session = liveblocks.prepareSession("olivier@example.com");

// Giving full access to one room
session.allow("design:9Hdu73", session.FULL_ACCESS);

// Give full access to every room with an ID beginning with "product:"
session.allow("product:*", session.FULL_ACCESS);

// Give read-only access to every room in the current tenant
session.allow("*", session.READ_ACCESS);

const { body, status } = await session.authorize();
```

### Limitations [#limitations]

There’s a limitation with access tokens related to granting access to individual
rooms that are part of groups. Let’s say a user has been given access to every
`product` room in their tenants.

```tsx
// Access to every `product` room
session.allow("product:*", session.FULL_ACCESS);
```

This user is able to enter `product` rooms, but has no access to any `design`
rooms.

<Figure>
  <Image
    src="/assets/authentication/access-token-product-access.png"
    alt="An access token using a wildcard to access product rooms"
    width={1494}
    height={840}
    quality={100}
  />
</Figure>

Let’s say the user is invited to a `design` room via share menu—how would we
grant them access?

<Figure>
  <Image
    src="/assets/authentication/access-token-room-invite.png"
    alt="Inviting Olivier to the `design:9Hdu73` room"
    width={1494}
    height={840}
    quality={100}
  />
</Figure>

We can’t give them access to _every_ `design` room with a wildcard, as they
should only have permission for _one_.

```tsx
// ❌ Access to every `design` room
session.allow("design:*", session.FULL_ACCESS);
```

Instead, we would have to manually find the exact room ID without a wildcard,
and apply it ourselves—the naming pattern doesn’t work for this room.

```tsx
// Access to just this `design` room, but not scalable
session.allow("design:9Hdu73", session.FULL_ACCESS);
```

To use access tokens you’d have to manually keep track of every room ID where
the naming pattern doesn’t apply. This isn’t ideal, and it also doesn’t scale,
as the token will need to be refreshed whenever access is granted to new rooms
for this to work correctly.

#### Building complex permissions

For this reason,
**[we recommend using ID tokens for complex permissions](/docs/authentication/id-token)**.
ID token authentication allows you to attach permissions to each room when it’s
created or modified, which means you don’t need to check permissions yourself,
and no naming pattern is required.

### Migrating your current rooms IDs

If your application already has rooms, it’s possible to rename their IDs to be
compatible with a naming pattern. Learn more in our
[room ID migration guide](/docs/guides/how-to-rename-room-ids-and-successfully-migrate-users).

## Select your framework [#select-your-framework]

Select your framework for specific instructions on setting up access token
authentication.

<ListGrid columns={3}>
  <DocsCard
    title="Next.js"
    href="/docs/authentication/access-token/nextjs"
    visual={<DocsNextjsIcon />}
  />
  <DocsCard
    title="Remix"
    href="/docs/authentication/access-token/remix"
    visual={<DocsRemixIcon />}
  />
  <DocsCard
    title="SvelteKit"
    href="/docs/authentication/access-token/sveltekit"
    visual={<DocsSvelteIcon />}
  />
  <DocsCard
    title="Nuxt.js"
    href="/docs/authentication/access-token/nuxtjs"
    visual={<DocsNuxtjsIcon />}
  />
  <DocsCard
    title="Express"
    href="/docs/authentication/access-token/express"
    visual={<DocsExpressIcon />}
  />
  <DocsCard
    title="Firebase"
    href="/docs/authentication/access-token/firebase"
    visual={<DocsFirebaseIcon />}
  />
</ListGrid>

---
meta:
  title: "Set up access token permissions with Express"
  parentTitle: "Authentication"
  description: "Learn how to setup access token permissions with Express."
---

Follow the following steps to start configure your authentication endpoint where

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>

      Users need permission to interact with rooms, and you can
      permit access in an `api/liveblocks-auth` endpoint by
      creating the `liveblocks-auth.ts` file with the
      following code. In here you can implement your security and define
      the rooms that your user can enter.

      With access tokens, you should always use a [naming pattern](/docs/authentication/access-token#permissions)
      for your room IDs, as this enables you to easily allow
      access to a range of rooms at once. In the code snippet below, we’re using a naming pattern and wildcard `*`
      to give the user access to every room in their organization, and every room in their group.


      ```ts file="liveblocks-auth.ts"
      const express = require("express");
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      const app = express();
      app.use(express.json());

      app.post("/api/liveblocks-auth", (req, res) => {
        // Get the current user from your database
        const user = __getUserFromDB__(req);

        // Start an auth session inside your endpoint
        const session = liveblocks.prepareSession(
          user.id,
          { userInfo: user.metadata },  // Optional
        );

        // Use a naming pattern to allow access to rooms with wildcards
        // Giving the user read access on their org, and write access on their group
        session.allow(`${user.organization}:*`, session.READ_ACCESS);
        session.allow(`${user.organization}:${user.group}:*`, session.FULL_ACCESS);

        // Authorize the user and return the result
        const { status, body } = await session.authorize();
        return res.status(status).end(body);
      });
      ```

      Read
      [access token permission](/docs/authentication/access-token#permissions)
      to learn more about naming rooms and granting permissions with wildcards.
      Note that if a naming pattern doesn’t work for every room in your application, you can
      [grant access to individual rooms too](/docs/guides/how-to-grant-access-to-individual-rooms-with-access-tokens).

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      option with `authEndpoint` pointing to the endpoint you
      just created.

      ```ts file="liveblocks.config.ts"
      import { createClient } from "@liveblocks/client";

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

      If you need to pass custom headers or data to your endpoint, you can
      use
      [authEndpoint as a callback](/docs/api-reference/liveblocks-client#createClientCallback)
      instead.

      ```ts file="liveblocks.config.ts" isCollapsed isCollapsable
      import { createClient } from "@liveblocks/client";

      // Passing custom headers and body to your endpoint
      const client = createClient({
        authEndpoint: async (room) => {
          const headers = {
            // Custom headers
            // ...

            "Content-Type": "application/json",
          };

          const body = JSON.stringify({
            // Custom body
            // ...

            room,
          });

          const response = await fetch("/api/liveblocks-auth", {
            method: "POST",
            headers,
            body,
          });

          return await response.json();
        },
      });
      ```

    </StepContent>

  </Step>
</Steps>

## More information

Both `userId` and `userInfo` can then be used in your JavaScript application as
such:

```ts
const self = room.getSelf(); // or useSelf() in React
console.log(self.id);
console.log(self.info);
```

<Figure>
  <Image
    src="/assets/access-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up access token permissions with Firebase"
  parentTitle: "Authentication"
  description: "Learn how to setup access token permissions with Firebase."
---

Follow the following steps to start configure your authentication endpoint and
start building your own security logic.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>
      Let’s first install the `@liveblocks/node` package in your
      Firebase functions project.

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>

      Users need permission to interact with rooms, and you can
      permit access by creating a new Firebase [callable function](https://firebase.google.com/docs/functions/callable)
      as shown below. In here you can implement your security and define
      the rooms that your user can enter.

      With access tokens, you should always use a [naming pattern](/docs/authentication/access-token#permissions)
      for your room IDs, as this enables you to easily allow
      access to a range of rooms at once. In the code snippet below, we’re using a naming pattern and wildcard `*`
      to give the user access to every room in their organization, and every room in their group.


      ```js
      const functions = require("firebase-functions");
      const { Liveblocks } = require("@liveblocks/node");

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      exports.auth = functions.https.onCall(async (data, context) => {
        // Get the current user from your database
        const user = __getUserFromDB__(data);

        // Start an auth session inside your endpoint
        const session = liveblocks.prepareSession(
          user.id,
          { userInfo: user.metadata },  // Optional
        );

        // Use a naming pattern to allow access to rooms with wildcards
        // Giving the user read access on their org, and write access on their group
        session.allow(`${user.organization}:*`, session.READ_ACCESS);
        session.allow(`${user.organization}:${user.group}:*`, session.FULL_ACCESS);

        // Authorize the user and return the result
        const { status, body } = await session.authorize();
        return JSON.parse(body);
      });
      ```

      Read
      [access token permission](/docs/authentication/access-token#permissions)
      to learn more about naming rooms and granting permissions with wildcards.
      Note that if a naming pattern doesn’t work for every room in your application, you can
      [grant access to individual rooms too](/docs/guides/how-to-grant-access-to-individual-rooms-with-access-tokens).


    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      option with `authEndpoint` pointing to the endpoint you
      just created.

      ```js
      import { createClient } from "@liveblocks/client";
      import firebase from "firebase";
      import "firebase/functions";

      firebase.initializeApp({
        /* Firebase config */
      });

      const auth = firebase.functions().httpsCallable("liveblocks-auth");

      // Create a Liveblocks client
      const client = createClient({
        authEndpoint: async (room) => (await auth({ room })).data,
      });
      ```

    </StepContent>

  </Step>
</Steps>

## More information

Both `userId` and `userInfo` can then be used in your JavaScript application as
such:

```ts
const self = room.getSelf(); // or useSelf() in React
console.log(self.id);
console.log(self.info);
```

<Figure>
  <Image
    src="/assets/access-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up access token permissions with Next.js"
  parentTitle: "Authentication"
  description: "Learn how to setup access token permissions with Next.js."
---

Follow the following steps to start configure your authentication endpoint and
start building your own security logic in Next.js’ `/app` directory.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add your project’s secret key</StepTitle>
    <StepContent>

      Create a new `.env.local` file and add your Liveblocks secret key from the [dashboard](/dashboard/apikeys).

      ```env file=".env.local"
      LIVEBLOCKS_SECRET_KEY="{{SECRET_KEY}}"
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>
      Users need permission to interact with rooms, and you can
      permit access in an `api/liveblocks-auth` endpoint by
      creating the following `app/api/liveblocks-auth/route.ts`
      file. In here you can implement your security and define
      the rooms that your user can enter.

      With access tokens, you should always use a [naming pattern](/docs/authentication/access-token#permissions)
      for your room IDs, as this enables you to easily allow
      access to a range of rooms at once. In the code snippet below, we’re using a naming pattern and wildcard `*`
      to give the user access to every room in their organization, and every room in their group.

      ```ts file="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) {
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Start an auth session inside your endpoint
        const session = liveblocks.prepareSession(
          user.id,
          { userInfo: user.metadata } // Optional
        );

        // Use a naming pattern to allow access to rooms with wildcards
        // Giving the user read access on their org, and write access on their group
        session.allow(`${user.organization}:*`, session.READ_ACCESS);
        session.allow(`${user.organization}:${user.group}:*`, session.FULL_ACCESS);

        // Authorize the user and return the result
        const { status, body } = await session.authorize();
        return new Response(body, { status });
      }
      ```

      Read
      [access token permission](/docs/authentication/access-token#permissions)
      to learn more about naming rooms and granting permissions with wildcards.
      Note that if a naming pattern doesn’t work for every room in your application, you can
      [grant access to individual rooms too](/docs/guides/how-to-grant-access-to-individual-rooms-with-access-tokens).

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      prop on [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
      with `authEndpoint` pointing to the endpoint you just created.

      ```tsx
      <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
      ```

      If you need to pass custom headers or data to your endpoint, you can
      use
      [authEndpoint as a callback](/docs/api-reference/liveblocks-client#LiveblocksProviderCallback)
      instead.

      ```tsx title="Pass custom headers" isCollapsed isCollapsable
      <LiveblocksProvider
        authEndpoint={async (room) => {
          // Passing custom headers and body to your endpoint
          const headers = {
            // Custom headers
            // ...

            "Content-Type": "application/json",
          };

          const body = JSON.stringify({
            // Custom body
            // ...

            room,
          });

          const response = await fetch("/api/liveblocks-auth", {
            method: "POST",
            headers,
            body,
          });

          return await response.json();
        }}
      />
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Attach metadata to users</StepTitle>
      <StepContent>
        Optionally, you can attach static metadata to each user, which will
        be accessible in your app. First you need to define the types in
        your config file, under `UserMeta["info"]`.

        ```ts file="liveblocks.config.ts" highlight="7-11"
        declare global
          interface Liveblocks {
            UserMeta: {
              id: string;

              // Example, use any JSON-compatible data in your metadata
              info: {
                name: string;
                avatar: string;
                colors: string[];
              }
            }

            // Other type definitions
            // ...
          }
        }
        ```

        When authenticating, you can then pass the user’s metadata to
        `prepareSession` in the endpoint we’ve just created.

        ```ts file="app/api/liveblocks-auth/route.ts" highlight="8-12"
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Start an auth session inside your endpoint
        const session = liveblocks.prepareSession(
          user.id,
          {
            userInfo: {
              name: user.name,
              avatar: user.avatarUrl,
              colors: user.colorArray,
            }
          }
        );
        ```

        User metadata has now been set! You can access this information in your app through
        [`useSelf`](/docs/api-reference/liveblocks-react#useSelf).

        ```tsx highlight="4"
        export { useSelf } from "@liveblocks/react/suspense";

        function Component() {
          const { name, avatar, colors } = useSelf((me) => me.info);
        }
        ```

        Bear in mind that if you’re using the [default Comments components](/docs/api-reference/liveblocks-react-ui#Components),
        you must specify a `name` and `avatar` in `userInfo`.
      </StepContent>

    </Step>

</Steps>

## More information

Both `userId` and `userInfo` can then be used in your React application as such:

```ts
const self = useSelf();
console.log(self.id);
console.log(self.info);
```

<Figure>
  <Image
    src="/assets/access-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up access token permissions with Nuxt.js"
  parentTitle: "Authentication"
  description: "Learn how to setup access token permissions with Nuxt.js."
---

Follow the following steps to start configure your authentication endpoint and
start building your own security logic.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>
      Users need permission to interact with rooms, and you can
      permit access in an `api/liveblocks-auth` endpoint by
      creating the `server/api/liveblocks-auth.ts` file with the
      following code. This is where you will implement your security and
      define the rooms that the user has access to.

      With access tokens, you should always use a [naming pattern](/docs/authentication/access-token#permissions)
      for your room IDs, as this enables you to easily allow
      access to a range of rooms at once. In the code snippet below, we’re using a naming pattern and wildcard `*`
      to give the user access to every room in their organization, and every room in their group.

      ```ts file="server/api/liveblocks-auth.ts"
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      export default defineEventHandler(async (event) => {
      // Get the current user from your database
        const user = __getUserFromDB__(event);

        // Start an auth session inside your endpoint
        const session = liveblocks.prepareSession(
          user.id,
          { userInfo: user.metadata },  // Optional
        );

        // Use a naming pattern to allow access to rooms with wildcards
        // Giving the user read access on their org, and write access on their group
        session.allow(`${user.organization}:*`, session.READ_ACCESS);
        session.allow(`${user.organization}:${user.group}:*`, session.FULL_ACCESS);

        // Authorize the user and return the result
        const { status, body } = await session.authorize();
        return body;
      })
      ```

      Read
      [access token permission](/docs/authentication/access-token#permissions)
      to learn more about naming rooms and granting permissions with wildcards.
      Note that if a naming pattern doesn’t work for every room in your application, you can
      [grant access to individual rooms too](/docs/guides/how-to-grant-access-to-individual-rooms-with-access-tokens).

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      option with `authEndpoint` pointing to the endpoint you
      just created.

      ```ts file="liveblocks.config.ts"
      import { createClient } from "@liveblocks/client";

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

      If you need to pass custom headers or data to your endpoint, you can
      use
      [authEndpoint as a callback](/docs/api-reference/liveblocks-client#createClientCallback)
      instead.

      ```ts file="liveblocks.config.ts" isCollapsed isCollapsable
      import { createClient } from "@liveblocks/client";

      // Passing custom headers and body to your endpoint
      const client = createClient({
        authEndpoint: async (room) => {
          const headers = {
            // Custom headers
            // ...

            "Content-Type": "application/json",
          };

          const body = JSON.stringify({
            // Custom body
            // ...

            room,
          });

          const response = await fetch("/api/liveblocks-auth", {
            method: "POST",
            headers,
            body,
          });

          return await response.json();
        },
      });
      ```

    </StepContent>

  </Step>
</Steps>

## More information

Both `userId` and `userInfo` can then be used in your Vue.js application as
such:

```ts
const self = ref(room.getSelf());
console.log(self.id);
console.log(self.info);
```

<Figure>
  <Image
    src="/assets/access-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up access token permissions with Remix"
  parentTitle: "Authentication"
  description: "Learn how to setup access token permissions with Remix."
---

Follow the following steps to start configure your authentication endpoint and
start building your own security logic.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>
      Users need permission to interact with rooms, and you can
      permit access in an `api/liveblocks-auth` endpoint by
      creating the `app/routes/api/liveblocks-auth.ts` file with the
      following code. In here you can implement your security and define
      the rooms that your user can enter.

      With access tokens, you should always use a [naming pattern](/docs/authentication/access-token#permissions)
      for your room IDs, as this enables you to easily allow
      access to a range of rooms at once. In the code snippet below, we’re using a naming pattern and wildcard `*`
      to give the user access to every room in their organization, and every room in their group.


      ```ts file="app/routes/api/liveblocks-auth.ts"
      import type { ActionFunction } from "@remix-run/node";
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      export const action: ActionFunction = async ({ request }) => {
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Start an auth session inside your endpoint
        const session = liveblocks.prepareSession(
          user.id,
          { userInfo: user.metadata },  // Optional
        );

        // Use a naming pattern to allow access to rooms with wildcards
        // Giving the user read access on their org, and write access on their group
        session.allow(`${user.organization}:*`, session.READ_ACCESS);
        session.allow(`${user.organization}:${user.group}:*`, session.FULL_ACCESS);

        // Authorize the user and return the result
        const { status, body } = await session.authorize();
        return new Response(body, { status });
      }
      ```

      Read
      [access token permission](/docs/authentication/access-token#permissions)
      to learn more about naming rooms and granting permissions with wildcards.
      Note that if a naming pattern doesn’t work for every room in your application, you can
      [grant access to individual rooms too](/docs/guides/how-to-grant-access-to-individual-rooms-with-access-tokens).

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      prop on [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
      with `authEndpoint` pointing to the endpoint you just created.

      ```tsx
      <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
      ```

      If you need to pass custom headers or data to your endpoint, you can
      use
      [authEndpoint as a callback](/docs/api-reference/liveblocks-client#LiveblocksProviderCallback)
      instead.

      ```tsx title="Pass custom headers" isCollapsed isCollapsable
      <LiveblocksProvider
        authEndpoint={async (room) => {
          // Passing custom headers and body to your endpoint
          const headers = {
            // Custom headers
            // ...

            "Content-Type": "application/json",
          };

          const body = JSON.stringify({
            // Custom body
            // ...

            room,
          });

          const response = await fetch("/api/liveblocks-auth", {
            method: "POST",
            headers,
            body,
          });

          return await response.json();
        }}
      />
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Attach metadata to users</StepTitle>
      <StepContent>
        Optionally, you can attach static metadata to each user, which will
        be accessible in your app. First you need to define the types in
        your config file, under `UserMeta["info"]`.

        ```ts file="liveblocks.config.ts" highlight="7-11"
        declare global
          interface Liveblocks {
            UserMeta: {
              id: string;

              // Example, use any JSON-compatible data in your metadata
              info: {
                name: string;
                avatar: string;
                colors: string[];
              }
            }

            // Other type definitions
            // ...
          }
        }
        ```

        When authenticating, you can then pass the user’s metadata to
        `prepareSession` in the endpoint we’ve just created.

        ```ts file="app/routes/api/liveblocks-auth.ts" highlight="8-12"
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Start an auth session inside your endpoint
        const session = liveblocks.prepareSession(
          user.id,
          {
            userInfo: {
              name: user.name,
              avatar: user.avatarUrl,
              colors: user.colorArray,
            }
          }
        );
        ```

        User metadata has now been set! You can access this information in your app through
        [`useSelf`](/docs/api-reference/liveblocks-react#useSelf).

        ```tsx highlight="4"
        export { useSelf } from "../liveblocks.config.ts";

        function Component() {
          const { name, avatar, colors } = useSelf((me) => me.info);
        }
        ```

        Bear in mind that if you’re using the [default Comments components](/docs/api-reference/liveblocks-react-ui#Components),
        you must specify a `name` and `avatar` in `userInfo`.
      </StepContent>

    </Step>

</Steps>

## More information

Both `userId` and `userInfo` can then be used in your React application as such:

```ts
const self = useSelf();
console.log(self.id);
console.log(self.info);
```

<Figure>
  <Image
    src="/assets/access-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up access token permissions with SvelteKit"
  parentTitle: "Authentication"
  description: "Learn how to setup access token permissions with SvelteKit."
---

Follow the following steps to start configure your authentication endpoint and
start building your own security logic.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>
      Users need permission to interact with rooms, and you can
      permit access in an `api/liveblocks-auth` endpoint by
      creating the `src/routes/api/liveblocks-auth/+server.ts` file with the
      following code. In here you can implement your security and define
      the rooms that your user can enter.

      With access tokens, you should always use a [naming pattern](/docs/authentication/access-token#permissions)
      for your room IDs, as this enables you to easily allow
      access to a range of rooms at once. In the code snippet below, we’re using a naming pattern and wildcard `*`
      to give the user access to every room in their organization, and every room in their group.


      ```ts file="src/routes/api/liveblocks-auth/+server.ts"
      import { type RequestEvent } from "@sveltejs/kit";
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      export async function POST({ request }: RequestEvent) {
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Start an auth session inside your endpoint
        const session = liveblocks.prepareSession(
          user.id,
          { userInfo: user.metadata },  // Optional
        );

        // Use a naming pattern to allow access to rooms with wildcards
        // Giving the user read access on their org, and write access on their group
        session.allow(`${user.organization}:*`, session.READ_ACCESS);
        session.allow(`${user.organization}:${user.group}:*`, session.FULL_ACCESS);

        // Authorize the user and return the result
        const { status, body } = await session.authorize();
        return new Response(body, { status });
      }
      ```

      Read
      [access token permission](/docs/authentication/access-token#permissions)
      to learn more about naming rooms and granting permissions with wildcards.
      Note that if a naming pattern doesn’t work for every room in your application, you can
      [grant access to individual rooms too](/docs/guides/how-to-grant-access-to-individual-rooms-with-access-tokens).

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      option with `authEndpoint` pointing to the endpoint you
      just created.

      ```ts file="liveblocks.config.ts"
      import { createClient } from "@liveblocks/client";

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

      If you need to pass custom headers or data to your endpoint, you can
      use
      [authEndpoint as a callback](/docs/api-reference/liveblocks-client#createClientCallback)
      instead.

      ```ts file="liveblocks.config.ts" isCollapsed isCollapsable
      import { createClient } from "@liveblocks/client";

      // Passing custom headers and body to your endpoint
      const client = createClient({
        authEndpoint: async (room) => {
          const headers = {
            // Custom headers
            // ...

            "Content-Type": "application/json",
          };

          const body = JSON.stringify({
            // Custom body
            // ...

            room,
          });

          const response = await fetch("/api/liveblocks-auth", {
            method: "POST",
            headers,
            body,
          });

          return await response.json();
        },
      });
      ```

    </StepContent>

  </Step>
</Steps>

## More information

Both `userId` and `userInfo` can then be used in your Svelte application as
such:

```ts
const self = room.getSelf();
console.log(self.id);
console.log(self.info.color);
```

<Figure>
  <Image
    src="/assets/access-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up ID token permissions with Express"
  parentTitle: "Authentication"
  description: "Learn how to setup ID token permissions with Express."
---

Follow the following steps to start configure your authentication endpoint where

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>

      Users can only interact with rooms they have access to. You can
      configure permission access in an `api/liveblocks-auth` endpoint by
      creating the `liveblocks-auth.ts` file with the
      following code. This is where you will implement your security and
      define if the current user has access to a specific room.

      ```ts file="liveblocks-auth.ts"
      const express = require("express");
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      const app = express();
      app.use(express.json());

      app.post("/api/liveblocks-auth", (req, res) => {
        // Get the current user from your database
        const user = __getUserFromDB__(req);

        // Identify the user and return the result
        const { status, body } = await liveblocks.identifyUser(
          {
            userId: user.id,
            groupIds, // Optional
          },
          { userInfo: user.metadata },
        );

        return res.status(status).end(body);
      });
      ```
    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      option with `authEndpoint` pointing to the endpoint you
      just created.

      ```ts file="liveblocks.config.ts"
      import { createClient } from "@liveblocks/client";

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

      If you need to pass custom headers or data to your endpoint, you can
      use
      [authEndpoint as a callback](/docs/api-reference/liveblocks-client#createClientCallback)
      instead.

      ```ts file="liveblocks.config.ts" isCollapsed isCollapsable
      import { createClient } from "@liveblocks/client";

      // Passing custom headers and body to your endpoint
      const client = createClient({
        authEndpoint: async (room) => {
          const headers = {
            // Custom headers
            // ...

            "Content-Type": "application/json",
          };

          const body = JSON.stringify({
            // Custom body
            // ...

            room,
          });

          const response = await fetch("/api/liveblocks-auth", {
            method: "POST",
            headers,
            body,
          });

          return await response.json();
        },
      });
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Set permission accesses to a room</StepTitle>
    <StepContent>
      A room can have `defaultAccesses`, `usersAccesses`, and `groupsAccesses` defined.
      Permissions are then checked when users try to connect to a room. For security purposes,
      [room permissions](/docs/authentication/id-token#permissions) can only be set on the back-end through `@liveblocks/node` or our REST API.
      For instance, you can use [`liveblocks.createRoom`](/docs/api-reference/liveblocks-node#post-rooms)
      to create a new room with read-only public access levels while giving write access to specific groups and users.

      ```ts highlight="7-15"
        import { Liveblocks } from "@liveblocks/node";

        const liveblocks = new Liveblocks({
          secret: "{{SECRET_KEY}}",
        });

        const room = await liveblocks.createRoom("my-room-id", {
          defaultAccesses: ["room:read", "room:presence:write"],
          groupsAccesses: {
            "my-group-id": ["room:write"],
          },
          usersAccesses: {
            "my-user-id": ["room:write"],
          },
        });
      ```

      For more information, make sure to read the section on [room permissions](/docs/authentication/id-token#permissions).

    </StepContent>

  </Step>
</Steps>

## More information

Both `userId` and `userInfo` can then be used in your JavaScript application as
such:

```ts
const self = room.getSelf(); // or useSelf() in React
console.log(self.id);
console.log(self.info);
```

<Figure>
  <Image
    src="/assets/id-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up ID token permissions with Firebase"
  parentTitle: "Authentication"
  description: "Learn how to setup ID token permissions with Firebase."
---

Follow the following steps to start configure your authentication endpoint and
start building your own security logic.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>
      Let’s first install the `@liveblocks/node` package in your
      Firebase functions project.

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>

      Create a new Firebase [callable function](https://firebase.google.com/docs/functions/callable)
      as shown below. This is where you will implement your security and
      define if the current user has access to a specific room.

      ```js
      const functions = require("firebase-functions");
      const { Liveblocks } = require("@liveblocks/node");

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      exports.auth = functions.https.onCall(async (data, context) => {
        // Get the current user from your database
        const user = __getUserFromDB__(data);

        // Identify the user and return the result
        const { status, body } = await liveblocks.identifyUser(
          {
            userId: user.id,
            groupIds, // Optional
          },
          { userInfo: user.metadata },
        );

        return JSON.parse(body);
      });
      ```
    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      option with `authEndpoint` pointing to the endpoint you
      just created.

      ```js
      import { createClient } from "@liveblocks/client";
      import firebase from "firebase";
      import "firebase/functions";

      firebase.initializeApp({
        /* Firebase config */
      });

      const auth = firebase.functions().httpsCallable("liveblocks-auth");

      // Create a Liveblocks client
      const client = createClient({
        authEndpoint: async (room) => (await auth({ room })).data,
      });
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Set permission accesses to a room</StepTitle>
    <StepContent>
      A room can have `defaultAccesses`, `usersAccesses`, and `groupsAccesses` defined.
      Permissions are then checked when users try to connect to a room. For security purposes,
      [room permissions](/docs/authentication/id-token#permissions) can only be set on the back-end through `@liveblocks/node` or our REST API.
      For instance, you can use [`liveblocks.createRoom`](/docs/api-reference/liveblocks-node#post-rooms)
      to create a new room with read-only public access levels while giving write access to specific groups and users.

      ```ts highlight="7-15"
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      const room = await liveblocks.createRoom("my-room-id", {
        defaultAccesses: ["room:read", "room:presence:write"],
        groupsAccesses: {
          "my-group-id": ["room:write"],
        },
        usersAccesses: {
          "my-user-id": ["room:write"],
        },
      });
      ```

      For more information, make sure to read the section on [room permissions](/docs/authentication/id-token#permissions).

    </StepContent>

  </Step>
</Steps>

## More information

Both `userId` and `userInfo` can then be used in your JavaScript application as
such:

```ts
const self = room.getSelf(); // or useSelf() in React
console.log(self.id);
console.log(self.info);
```

<Figure>
  <Image
    src="/assets/id-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up ID token permissions with Next.js"
  parentTitle: "Authentication"
  description: "Learn how to setup ID token permissions with Next.js."
---

Follow the following steps to start configure your authentication endpoint and
start building your own security logic in Next.js’ `/app` directory.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add your project’s secret key</StepTitle>
    <StepContent>

      Create a new `.env.local` file and add your Liveblocks secret key from the [dashboard](/dashboard/apikeys).

      ```env file=".env.local"
      LIVEBLOCKS_SECRET_KEY="{{SECRET_KEY}}"
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>
      Users can only interact with rooms they have access to. You can
      configure permission access in an `api/liveblocks-auth` endpoint by
      creating the `app/api/liveblocks-auth/route.ts` file with the
      following code. This is where you will implement your security and
      define if the current user has access to a specific room.

      ```ts file="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) {
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Identify the user and return the result
        const { status, body } = await liveblocks.identifyUser(
          {
            userId: user.id,
            groupIds, // Optional
          },
          { userInfo: user.metadata },
        );

        return new Response(body, { status });
      }
      ```

      Here’s an example using the older API routes format in `/pages`.

      ```ts file="pages/api/liveblocks-auth.ts" isCollapsed isCollapsable
      import { Liveblocks } from "@liveblocks/node";
      import type { NextApiRequest, NextApiResponse } from "next";

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

      export default async function handler(request: NextApiRequest, response: NextApiResponse) {
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Identify the user and return the result
        const { status, body } = await liveblocks.identifyUser(
          {
            userId: user.id,
            groupIds, // Optional
          },
          { userInfo: user.metadata },
        );

        // Authorize the user and return the result
        const { status, body } = await session.authorize();
        response.status(status).send(body);
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      prop on [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
      with `authEndpoint` pointing to the endpoint you just created.

      ```tsx
      <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
      ```

      If you need to pass custom headers or data to your endpoint, you can
      use
      [authEndpoint as a callback](/docs/api-reference/liveblocks-client#LiveblocksProviderCallback)
      instead.

      ```tsx title="Pass custom headers" isCollapsed isCollapsable
      <LiveblocksProvider
        authEndpoint={async (room) => {
          // Passing custom headers and body to your endpoint
          const headers = {
            // Custom headers
            // ...

            "Content-Type": "application/json",
          };

          const body = JSON.stringify({
            // Custom body
            // ...

            room,
          });

          const response = await fetch("/api/liveblocks-auth", {
            method: "POST",
            headers,
            body,
          });

          return await response.json();
        }}
      />
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set permission accesses to a room</StepTitle>
    <StepContent>
      A room can have `defaultAccesses`, `usersAccesses`, and `groupsAccesses` defined.
      Permissions are then checked when users try to connect to a room. For security purposes,
      [room permissions](/docs/authentication/id-token#permissions) can only be set on the back-end through `@liveblocks/node` or our REST API.
      For instance, you can use [`liveblocks.createRoom`](/docs/api-reference/liveblocks-node#post-rooms)
      to create a new room with read-only public access levels while giving write access to specific groups and users.

      ```ts highlight="7-15"
      import { Liveblocks } from "@liveblocks/node";

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

      const room = await liveblocks.createRoom("my-room-id", {
        defaultAccesses: ["room:read", "room:presence:write"],
        groupsAccesses: {
          "my-group-id": ["room:write"],
        },
        usersAccesses: {
          "my-user-id": ["room:write"],
        },
      });
      ```

      For more information, make sure to read the section on [room permissions](/docs/authentication/id-token#permissions).

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Attach metadata to users</StepTitle>
      <StepContent>
        Optionally, you can attach static metadata to each user, which will
        be accessible in your app. First you need to define the types in
        your config file, under `UserMeta["info"]`.

        ```ts file="liveblocks.config.ts" highlight="7-11"
        declare global
          interface Liveblocks {
            UserMeta: {
              id: string;

              // Example, use any JSON-compatible data in your metadata
              info: {
                name: string;
                avatar: string;
                colors: string[];
              }
            }

            // Other type definitions
            // ...
          }
        }
        ```

        When authenticating, you can then pass the user’s metadata to
        `prepareSession` in the endpoint we’ve just created.

        ```ts file="app/api/liveblocks-auth/route.ts" highlight="11-15"
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Identify the user and return the result
        const { status, body } = await liveblocks.identifyUser(
          {
            userId: user.id,
            groupIds, // Optional
          },
          {
            userInfo: {
              name: user.name,
              avatar: user.avatarUrl,
              colors: user.colorArray,
            }
          },
        );
        ```

        User metadata has now been set! You can access this information in your app through
        [`useSelf`](/docs/api-reference/liveblocks-react#useSelf).

        ```tsx highlight="4"
        export { useSelf } from "@liveblocks/react/suspense";

        function Component() {
          const { name, avatar, colors } = useSelf((me) => me.info);
        }
        ```

        Bear in mind that if you’re using the [default Comments components](/docs/api-reference/liveblocks-react-ui#Components),
        you must specify a `name` and `avatar` in `userInfo`.
      </StepContent>

    </Step>

</Steps>

## More information

Both `userId` and `userInfo` can then be used in your React application as such:

```ts
const self = useSelf();
console.log(self.id);
console.log(self.info);
```

<Figure>
  <Image
    src="/assets/id-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up ID token permissions with Nuxt.js"
  parentTitle: "Authentication"
  description: "Learn how to setup ID token permissions with Nuxt.js."
---

Follow the following steps to start configure your authentication endpoint and
start building your own security logic.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>
      Users can only interact with rooms they have access to. You can
      configure permission access in an `api/liveblocks-auth` endpoint by
      creating the `server/api/liveblocks-auth.ts` file with the
      following code. This is where you will implement your security and
      define if the current user has access to a specific room.

      ```ts file="server/api/liveblocks-auth.ts"
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      export default defineEventHandler(async (event) => {
        // Get the current user from your database
        const user = __getUserFromDB__(event);

        // Identify the user and return the result
        const { status, body } = await liveblocks.identifyUser(
          {
            userId: user.id,
            groupIds, // Optional
          },
          { userInfo: user.metadata },
        );
        return body;
      })
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      option with `authEndpoint` pointing to the endpoint you
      just created.

      ```ts file="liveblocks.config.ts"
      import { createClient } from "@liveblocks/client";

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

      If you need to pass custom headers or data to your endpoint, you can
      use
      [authEndpoint as a callback](/docs/api-reference/liveblocks-client#createClientCallback)
      instead.

      ```ts file="liveblocks.config.ts" isCollapsed isCollapsable
      import { createClient } from "@liveblocks/client";

      // Passing custom headers and body to your endpoint
      const client = createClient({
        authEndpoint: async (room) => {
          const headers = {
            // Custom headers
            // ...

            "Content-Type": "application/json",
          };

          const body = JSON.stringify({
            // Custom body
            // ...

            room,
          });

          const response = await fetch("/api/liveblocks-auth", {
            method: "POST",
            headers,
            body,
          });

          return await response.json();
        },
      });
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Set permission accesses to a room</StepTitle>
    <StepContent>
      A room can have `defaultAccesses`, `usersAccesses`, and `groupsAccesses` defined.
      Permissions are then checked when users try to connect to a room. For security purposes,
      [room permissions](/docs/authentication/id-token#permissions) can only be set on the back-end through `@liveblocks/node` or our REST API.
      For instance, you can use [`liveblocks.createRoom`](/docs/api-reference/liveblocks-node#post-rooms)
      to create a new room with read-only public access levels while giving write access to specific groups and users.

      ```ts highlight="7-15"
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      const room = await liveblocks.createRoom("my-room-id", {
        defaultAccesses: ["room:read", "room:presence:write"],
        groupsAccesses: {
          "my-group-id": ["room:write"],
        },
        usersAccesses: {
          "my-user-id": ["room:write"],
        },
      });
      ```

      For more information, make sure to read the section on [room permissions](/docs/authentication/id-token#permissions).

    </StepContent>

  </Step>
</Steps>

## More information

Both `userId` and `userInfo` can then be used in your Vue.js application as
such:

```ts
const self = ref(room.getSelf());
console.log(self.id);
console.log(self.info);
```

<Figure>
  <Image
    src="/assets/id-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up ID token permissions with Remix"
  parentTitle: "Authentication"
  description: "Learn how to setup ID token permissions with Remix."
---

Follow the following steps to start configure your authentication endpoint and
start building your own security logic.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>
      Users can only interact with rooms they have access to. You can
      configure permission access in an `api/liveblocks-auth` endpoint by
      creating the `app/routes/api/liveblocks-auth.ts` file with the
      following code. This is where you will implement your security and
      define if the current user has access to a specific room.

      ```ts file="app/routes/api/liveblocks-auth.ts"
      import type { ActionFunction } from "@remix-run/node";
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      export const action: ActionFunction = async ({ request }) => {
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Identify the user and return the result
        const { status, body } = await liveblocks.identifyUser(
          {
            userId: user.id,
            groupIds, // Optional
          },
          { userInfo: user.metadata },
        );

        return new Response(body, { status });
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      prop on [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
      with `authEndpoint` pointing to the endpoint you just created.

      ```tsx
      <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
      ```

      If you need to pass custom headers or data to your endpoint, you can
      use
      [authEndpoint as a callback](/docs/api-reference/liveblocks-client#LiveblocksProviderCallback)
      instead.

      ```tsx title="Pass custom headers" isCollapsed isCollapsable
      <LiveblocksProvider
        authEndpoint={async (room) => {
          // Passing custom headers and body to your endpoint
          const headers = {
            // Custom headers
            // ...

            "Content-Type": "application/json",
          };

          const body = JSON.stringify({
            // Custom body
            // ...

            room,
          });

          const response = await fetch("/api/liveblocks-auth", {
            method: "POST",
            headers,
            body,
          });

          return await response.json();
        }}
      />
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set permission accesses to a room</StepTitle>
    <StepContent>
      A room can have `defaultAccesses`, `usersAccesses`, and `groupsAccesses` defined.
      Permissions are then checked when users try to connect to a room. For security purposes,
      [room permissions](/docs/authentication/id-token#permissions) can only be set on the back-end through `@liveblocks/node` or our REST API.
      For instance, you can use [`liveblocks.createRoom`](/docs/api-reference/liveblocks-node#post-rooms)
      to create a new room with read-only public access levels while giving write access to specific groups and users.

      ```ts highlight="7-15"
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      const room = await liveblocks.createRoom("my-room-id", {
        defaultAccesses: ["room:read", "room:presence:write"],
        groupsAccesses: {
          "my-group-id": ["room:write"],
        },
        usersAccesses: {
          "my-user-id": ["room:write"],
        },
      });
      ```

      For more information, make sure to read the section on [room permissions](/docs/authentication/id-token#permissions).

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Attach metadata to users</StepTitle>
      <StepContent>
        Optionally, you can attach static metadata to each user, which will
        be accessible in your app. First you need to define the types in
        your config file, under `UserMeta["info"]`.

        ```ts file="liveblocks.config.ts" highlight="7-11"
        declare global
          interface Liveblocks {
            UserMeta: {
              id: string;

              // Example, use any JSON-compatible data in your metadata
              info: {
                name: string;
                avatar: string;
                colors: string[];
              }
            }

            // Other type definitions
            // ...
          }
        }
        ```

        When authenticating, you can then pass the user’s metadata to
        `prepareSession` in the endpoint we’ve just created.

        ```ts file="app/routes/api/liveblocks-auth.ts" highlight="11-15"
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Identify the user and return the result
        const { status, body } = await liveblocks.identifyUser(
          {
            userId: user.id,
            groupIds, // Optional
          },
          {
            userInfo: {
              name: user.name,
              avatar: user.avatarUrl,
              colors: user.colorArray,
            }
          },
        );
        ```

        User metadata has now been set! You can access this information in your app through
        [`useSelf`](/docs/api-reference/liveblocks-react#useSelf).

        ```tsx highlight="4"
        export { useSelf } from "@liveblocks/react/suspense";

        function Component() {
          const { name, avatar, colors } = useSelf((me) => me.info);
        }
        ```

        Bear in mind that if you’re using the [default Comments components](/docs/api-reference/liveblocks-react-ui#Components),
        you must specify a `name` and `avatar` in `userInfo`.
      </StepContent>

    </Step>

</Steps>

## More information

Both `userId` and `userInfo` can then be used in your React application as such:

```ts
const self = useSelf();
console.log(self.id);
console.log(self.info);
```

<Figure>
  <Image
    src="/assets/id-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Set up ID token permissions with SvelteKit"
  parentTitle: "Authentication"
  description: "Learn how to setup ID token permissions with SvelteKit."
---

Follow the following steps to start configure your authentication endpoint and
start building your own security logic.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install the `liveblocks/node` package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication endpoint</StepTitle>
    <StepContent>
      Users can only interact with rooms they have access to. You can
      configure permission access in an `api/liveblocks-auth` endpoint by
      creating the `src/routes/api/liveblocks-auth/+server.ts` file with the
      following code. This is where you will implement your security and
      define if the current user has access to a specific room.

      ```ts file="src/routes/api/liveblocks-auth/+server.ts"
      import { type RequestEvent } from "@sveltejs/kit";
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      export async function POST({ request }: RequestEvent) {
        // Get the current user from your database
        const user = __getUserFromDB__(request);

        // Identify the user and return the result
        const { status, body } = await liveblocks.identifyUser(
          {
            userId: user.id,
            groupIds, // Optional
          },
          { userInfo: user.metadata },
        );

        return new Response(body, { status });
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the client</StepTitle>
    <StepContent>
      On the front end, you can now replace the `publicApiKey`
      option with `authEndpoint` pointing to the endpoint you
      just created.

      ```ts file="liveblocks.config.ts"
      import { createClient } from "@liveblocks/client";

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

      If you need to pass custom headers or data to your endpoint, you can
      use
      [authEndpoint as a callback](/docs/api-reference/liveblocks-client#createClientCallback)
      instead.

      ```ts file="liveblocks.config.ts" isCollapsed isCollapsable
      import { createClient } from "@liveblocks/client";

      // Passing custom headers and body to your endpoint
      const client = createClient({
        authEndpoint: async (room) => {
          const headers = {
            // Custom headers
            // ...

            "Content-Type": "application/json",
          };

          const body = JSON.stringify({
            // Custom body
            // ...

            room,
          });

          const response = await fetch("/api/liveblocks-auth", {
            method: "POST",
            headers,
            body,
          });

          return await response.json();
        },
      });
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Set permission accesses to a room</StepTitle>
    <StepContent>
      A room can have `defaultAccesses`, `usersAccesses`, and `groupsAccesses` defined.
      Permissions are then checked when users try to connect to a room. For security purposes,
      [room permissions](/docs/authentication/id-token#permissions) can only be set on the back-end through `@liveblocks/node` or our REST API.
      For instance, you can use [`liveblocks.createRoom`](/docs/api-reference/liveblocks-node#post-rooms)
      to create a new room with read-only public access levels while giving write access to specific groups and users.

      ```ts highlight="7-15"
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      const room = await liveblocks.createRoom("my-room-id", {
        defaultAccesses: ["room:read", "room:presence:write"],
        groupsAccesses: {
          "my-group-id": ["room:write"],
        },
        usersAccesses: {
          "my-user-id": ["room:write"],
        },
      });
      ```

      For more information, make sure to read the section on [room permissions](/docs/authentication/id-token#permissions).

    </StepContent>

  </Step>
</Steps>

## More information

Both `userId` and `userInfo` can then be used in your Svelte application as
such:

```ts
const self = room.getSelf();
console.log(self.id);
console.log(self.info.color);
```

<Figure>
  <Image
    src="/assets/id-token-auth-diagram.png"
    alt="Auth diagram"
    width={768}
    height={576}
  />
</Figure>

---
meta:
  title: "Tenants"
  parentTitle: "Authentication"
  description: "Learn more about tenants"
---

Tenants allow you to compartmentalize Liveblocks resources, such as inbox
notifications, rooms, and everything associated with rooms such as comment
threads, realtime data stored, and more. Each tenant represents a separate
organization or customer in your system, meaning you can easily add a
workspace/org switcher to your application, with each workspace having its own
notification inbox.

<Banner title="Tenants is optional">

If you don’t specify a tenant, no configuration is needed. By default, resources
belong to the `default` tenant ID.

</Banner>

<Figure>
  <Image
    src="/assets/tenants/concept-tenants.png"
    alt="Concept of tenants"
    width={1494}
    height={840}
    quality={100}
  />
</Figure>

### Setting up tenants

To set up Tenants, you just need to set a `tenantId` when creating a resource,
for example, when creating a room.

```ts
const room = await liveblocks.createRoom("my-room-id", {
  // +++
  tenantId: "my-tenant-id",
  // +++
});
```

<Banner title="You can’t change a resource’s tenant" type="warning">

It’s not possible to change a resource’s `tenantId` after creation. You will
need to migrate the resource into a new room in the correct tenant.

</Banner>

#### Resources using tenants

A number of Liveblocks resources use tenants, such as rooms, comments, text
editors, Yjs, Storage, inbox notifications, mention groups.

```ts
const { data: rooms, nextCursor } = await liveblocks.getRooms({
  // +++
  tenantId: "my-tenant-id",
  // +++
});

await liveblocks.triggerInboxNotification({
  userId: "steven@example.com",
  kind: "$fileUploaded",
  subjectId: "my-file",
  activityData: {},
  // +++
  tenantId: "my-tenant-id",
  // +++
});

await liveblocks.deleteAllInboxNotifications({
  userId: "steven@example.com",
  // +++
  tenantId: "my-tenant-id",
  // +++
});

const { data, nextCursor } = await liveblocks.getUserRoomSubscriptionSettings({
  userId: "steven@example.com",
  // +++
  tenantId: "my-tenant-id",
  // +++
});
```

Above you can see [`getRooms`](/docs/api-reference/liveblocks-node#get-rooms),
[`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger),
[`deleteAllInboxNotifications`](/docs/api-reference/liveblocks-node#delete-users-userId-inboxNotifications)
and
[`getUserRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#get-users-userId-room-subscription-settings)
using tenants. Tenants can also be used with the
[REST API](/docs/api-reference/rest-api-endpoints).

### Authentication

When authorizing a user with Liveblocks, you can specify the tenant they belong
to. Each user is only authorized for one tenant at a time, meaning they need to
re-authenticate to access resources in another tenant, such as notifications.
Tenants can be used with both [ID Token](/docs/authentication/id-token) and
[Access Token](/docs/authentication/access-token) authentication.

#### With ID tokens

When using ID tokens, you can set the `tenantId` when using `identifyUser`.
Tokens generated for a specific tenant, will only allow access to resources
inside this tenant, even if the user has access to rooms in other tenants.

```ts highlight="3"
const { body, status } = await liveblocks.identifyUser({
  userId: "olivier@example.com",
  tenantId: "tenant123",
});

// '{ token: "eyJga7..." }'
console.log(body);
```

Learn more about [ID tokens](/docs/authentication/id-token).

#### With access tokens

When using access tokens, you can set the `tenantId` when you prepare a session.
Tokens generated for a specific tenant, will only allow access to resources
inside this tenant, even if the token has permissions to rooms in other tenants.

```ts highlight="2"
const session = liveblocks.prepareSession("olivier@example.com", {
  tenantId: "tenant123",
});

// Giving full access to one room
session.allow("Vu78Rt:design:9Hdu73", session.FULL_ACCESS);

// Give full access to every room with an ID beginning with "Vu78Rt:product:"
session.allow("Vu78Rt:product:*", session.FULL_ACCESS);

const { body, status } = await session.authorize();
```

Learn more about [access tokens](/docs/authentication/access-token).

#### Switching tenants

The easiest way to switch tenants is to refresh the page, for example with
`location.reload()`, then pass a new `tenantId` to your chosen authentication
method.

---
meta:
  title: "Concepts"
  description:
    "Liveblocks is the collaboration layer for your product. This page
    introduces the core concepts behind Liveblocks so you understand how
    everything fits together."
---

Liveblocks is the collaboration layer for your product. It provides the
architecture, realtime backend, and ready-made features that let humans and AI
collaborate inside your app—both in realtime and asynchronously. Think of
Liveblocks as the collaboration architecture your product never had. This page
introduces the core concepts behind Liveblocks so you understand how everything
fits together.

<Figure>
  <Image
    src="/assets/concepts/liveblocks-collaboration-infrastructure.png"
    alt="Liveblocks collaboration infrastructure"
    width={1344}
    height={1734}
    quality={100}
  />
</Figure>

## What is Liveblocks?

Liveblocks is the collaboration layer for your product. It’s built on a small
set of foundational concepts list below. These concepts work together to let
people and AI collaborate inside your app, without changing your existing
authentication, backend, or data model.

| Concept             | Description                                                                               |
| ------------------- | ----------------------------------------------------------------------------------------- |
| Projects            | Top-level container for your application                                                  |
| Tenants             | Compartmentalize rooms and notifications per organization                                 |
| Rooms               | Spaces in your product where collaboration happens, such as documents                     |
| Collaborators       | People (users) and AI agents doing work together                                          |
| Ready-made features | Collaboration building blocks like Comments, Multiplayer, Notifications, and AI Agents    |
| Platform            | The realtime backend, admin dashboard, and SDKs that make collaboration possible at scale |

<Figure>
  <Image
    src="/assets/concepts/liveblocks-structure.png"
    alt="Liveblocks structure"
    width={1344}
    height={760}
    quality={100}
  />
</Figure>

## Projects

A project is the top-level container in your Liveblocks team workspace that
usually maps to an application. Each project includes things like API keys,
rooms, AI agents, usage metrics, event logs, webhook settings, notification
settings, region and environment settings, and more.

### Regions

You can choose to locate a project in one of the following regions. The region
cannot be changed once the project has been created.

| Region | Data hosted                                           | Availability                |
| ------ | ----------------------------------------------------- | --------------------------- |
| Earth  | Globally on Cloudflare edge servers and AWS us‑east‑1 | All plans                   |
| US     | US only with Cloudflare FedRAMP and AWS us‑east‑1     | [Enterprise plan](/pricing) |
| EU     | EU only with Cloudflare EU and AWS eu‑central‑1       | [Enterprise plan](/pricing) |

### Environments

You can assign one of the following environment tags to a project. The
environment tag cannot be changed once the project has been created. Teams
typically create separate projects for production, staging, and development.
Encrypted keys and higher limits apply to production environment projects.

| Environment | Description                               |
| ----------- | ----------------------------------------- |
| Development | For development, testing, and prototyping |
| Production  | For production                            |

## Tenants

Tenants allow you to compartmentalize Liveblocks resources, such as inbox
notifications, rooms, and everything associated with rooms such as comment
threads, realtime data stored, and more. Each tenant represents a separate
organization or customer in your system, meaning you can easily add a
workspace/org switcher to your application, with each workspace having its own
notification inbox. Each project can have multiple tenants, though only one is
required.

## Rooms

A room is the space where people and AI collaborate together. Inside a room, you
can enable things like presence, shared state for multiplayer interactions,
contextual comment threads, @ mentions and actions that trigger notifications.

<Figure>
  <Image
    src="/assets/concepts/liveblocks-rooms.png"
    alt="Liveblocks rooms. The space where people and AI collaborate."
    width={1536}
    height={880}
    quality={100}
  />
</Figure>

Rooms typically map to artifacts people and AI can create inside your product,
such as text documents, workflow diagrams, spreadsheets, whiteboards, forms, 3D
files, video editors, design files, presentations, and more.

## Ready-made features

Liveblocks includes a set of collaboration building blocks you can use
individually or together. These ready-made features provide pre-built
components, hooks, and APIs that handle the complexity of realtime
collaboration, allowing you to focus on building your product’s unique
experience.

<ListGrid columns={2}>
  <DocsCardFeature
    feature="comments"
    href="/docs/ready-made-features/comments"
  />
  <DocsCardFeature
    feature="multiplayer"
    href="/docs/ready-made-features/multiplayer"
  />
  <DocsCardFeature
    feature="ai-agents"
    href="/docs/ready-made-features/ai-agents"
  />
  <DocsCardFeature
    feature="notifications"
    href="/docs/ready-made-features/notifications"
  />
</ListGrid>

## Collaborators

Collaborators are the entities that do work together in your application.
Liveblocks provides the infrastructure and ready-made features they use to
collaborate. There are two types of collaborators: users (people) and AI agents.

### Users

Users are the authenticated people using your application. Liveblocks only needs
a `userId` to identify them. Users can:

- View and edit content.
- See presence and activity.
- Leave contextual comments.
- @ mention users, groups, and AI agents.
- Receive notifications.

### AI Agents

AI agents are AI-driven collaborators that can act inside your product.
Liveblocks supports two types of agents: Liveblocks AI Copilots and framework
agents (private beta).

#### Liveblocks AI Copilots

Configured directly in the dashboard. Ideal for chatbot-style assistive
behavior.

- Works with any LLM (OpenAI, Anthropic, Gemini, or custom).
- Supports RAG knowledge from PDFs, images, websites, and text.
- Fully compatible with the
  [`<AiChat />`](/docs/ready-made-features/ai-agents/liveblocks-ai-copilots/default-components#AiChat)
  component.
- Acts on behalf of a user to take actions inside your app.
- Accessed through chat (not via @ mentions).

#### Framework agents (private beta)

Use external agents powered by your own infrastructure or tools like
[n8n](https://n8n.io/), [LangChain](https://www.langchain.com/),
[Crew AI](https://crewai.com/), and more. Framework agents behave like true
collaborators.

- They appear in rooms with presence.
- Users can @mention them in comments or text documents.
- Room events (mentions, edits, messages) are sent to your webhook endpoint.
- Your agent returns actions that update UI, create comments, or drive
  workflows.

<Banner type="info" title="Private beta">

Framework agents are currently in private beta. If you would like access to the
beta, please [contact us](https://liveblocks.io/contact/sales). We’d love to
hear from you.

</Banner>

## Platform

The Liveblocks Platform powers collaboration at runtime through a realtime back
end, using WebSocket infrastructure to handle presence, connection management,
low-latency messaging, and room orchestration. Async pipelines manage
notification delivery, webhooks, and event batching and grouping. An admin
dashboard provides usage and monitoring, project and API keys management,
webhook management, notification channel management, hosting regions, and event
logs. The platform also includes a
[DevTools browser extension](/docs/platform/devtools) and a comprehensive set of
SDKs and packages.

### SDKs and packages

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="JavaScript"
    href="/docs/api-reference/liveblocks-client"
    description="@liveblocks/client"
    visual={<DocsJavascriptIcon />}
  />
  <DocsCard
    type="technology"
    title="React"
    href="/docs/api-reference/liveblocks-react"
    description="@liveblocks/react"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="React UI"
    href="/docs/api-reference/liveblocks-react-ui"
    description="@liveblocks/react-ui"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="React Tiptap"
    href="/docs/api-reference/liveblocks-react-tiptap"
    description="@liveblocks/react-tiptap"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="React BlockNote"
    href="/docs/api-reference/liveblocks-react-blocknote"
    description="@liveblocks/react-blocknote"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Node.js ProseMirror"
    href="/docs/api-reference/liveblocks-node-prosemirror"
    description="@liveblocks/node-prosemirror"
    visual={<DocsNodejsIcon />}
  />
  <DocsCard
    type="technology"
    title="React Lexical"
    href="/docs/api-reference/liveblocks-react-lexical"
    description="@liveblocks/react-lexical"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Node.js Lexical"
    href="/docs/api-reference/liveblocks-node-lexical"
    description="@liveblocks/node-lexical"
    visual={<DocsNodejsIcon />}
  />
  <DocsCard
    type="technology"
    title="Redux"
    href="/docs/api-reference/liveblocks-redux"
    description="@liveblocks/redux"
    visual={<DocsReduxIcon />}
  />
  <DocsCard
    type="technology"
    title="Zustand"
    href="/docs/api-reference/liveblocks-zustand"
    description="@liveblocks/zustand"
    visual={
      <img
        alt=""
        src="/assets/zustand.png"
        width={24}
        height={24}
        loading="lazy"
      />
    }
  />
  <DocsCard
    type="technology"
    title="Yjs"
    href="/docs/api-reference/liveblocks-yjs"
    description="@liveblocks/yjs"
    visual={<DocsYjsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Node.js"
    href="/docs/api-reference/liveblocks-node"
    description="@liveblocks/node"
    visual={<DocsNodejsIcon />}
  />
  <DocsCard
    type="technology"
    title="Node.js Emails"
    href="/docs/api-reference/liveblocks-emails"
    description="@liveblocks/emails"
    visual={<DocsNodejsIcon />}
  />
  <DocsCard
    type="technology"
    title="REST API"
    href="/docs/api-reference/rest-api-endpoints"
    description="HTTP endpoints"
    visual={<DocsApiIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

---
meta:
  title:
    "At least one of the custom notification kinds you provided for
    'ActivitiesData' does not match its requirements"
  parentTitle: "Error"
  description: "Your 'ActivitiesData' type is incorrect and needs to be updated"
---

## Why this error occurred

You have provided a custom `ActivitiesData` type for your application, but the
type you provided isn’t a (completely) valid definition.

```ts highlight="4-8"
declare global {
  interface Liveblocks {
    ActivitiesData: {
      // Your custom notification kinds go here...
      $error: {
        code: number;
        date: Date; // ❌ Values must simple
      };
      // ❌ Custom notification kinds must start with $
      success: {
        message: string;
      };
    };
  }
}
```

In the example above, there are two problems. Activities data may only contain
simple key/value pairs, where the values must always be assignable to
`string | number | boolean | undefined` and custom notification kinds must start
with `$`.

## How to fix it

You’ll need to figure out what part of your provided `ActivitiesData` type
definition isn’t valid. The example above could be fixed as such:

```ts highlight="6-8"
declare global {
  interface Liveblocks {
    ActivitiesData: {
      // Your custom notification kinds go here...
      $error: {
        code: number;
        date: string;
      };
      $success: {
        message: string;
      };
    };
  }
}
```

## If you cannot find the root cause

Sometimes types can be complex and the root cause is still unclear. In those
cases, there is a small trick you can use. Try to assign your type to the
required base type, with this line:

```ts highlight="9-10"
import type { BaseActivitiesData } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    ActivitiesData: MyActivitiesData;
  }
}

// Quick debugging snippet to find root cause
const xxx: BaseActivitiesData = {} as MyActivitiesData;
//    ^?
//    The error will appear here
```

Now TypeScript will explain why it thinks your type isn’t valid:

```error showLineNumbers={false}
Type 'MyActivitiesData' is not assignable to type 'BaseActivitiesData'.
  Property 'date' is incompatible with index signature.
    Type 'Date' is not assignable to type 'string | number | boolean | undefined'.
```

---
meta:
  title:
    "The type you provided for 'CommentMetadata' does not match its requirements"
  parentTitle: "Error"
  description:
    "Your 'CommentMetadata' type is incorrect and needs to be updated"
---

## Why this error occurred

You have provided a custom `CommentMetadata` type for your application, but the
type you provided isn’t a (completely) valid definition.

```ts highlight="4-8"
declare global {
  interface Liveblocks {
    CommentMetadata: {
      // Your custom fields go here...
      spoiler: boolean;
      userAgent: string | null; // ❌ Values may not be null
      slack: { channelId: string; messageId: string }; // ❌ Values must be simple
      priority?: number;
    };
  }
}
```

In the example above, there are two problems. Comment metadata may only contain
simple key/value pairs, where the values must always be assignable to
`string | number | boolean | undefined`.

## How to fix it

You’ll need to figure out what part of your provided `CommentMetadata` type
definition isn’t valid. The example above could be fixed as such:

```ts highlight="6-8"
declare global {
  interface Liveblocks {
    CommentMetadata: {
      // Your custom fields go here...
      spoiler: boolean;
      userAgent?: string; // ✅
      slackChannelId: string; // ✅
      slackMessageId: string; // ✅
      priority?: number;
    };
  }
}
```

## If you cannot find the root cause

Sometimes types can be complex and the root cause is still unclear. In those
cases, there is a small trick you can use. Try to assign your type to the
required base type, with this line:

```ts highlight="9-10"
import type { BaseMetadata } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    CommentMetadata: MyCommentMetadata;
  }
}

// Quick debugging snippet to find root cause
const xxx: BaseMetadata = {} as MyCommentMetadata;
//    ^?
//    The error will appear here
```

Now TypeScript will explain why it thinks your type isn’t valid metadata:

```error showLineNumbers={false}
Type 'MyCommentMetadata' is not assignable to type 'BaseMetadata'.
  Property 'userAgent' is incompatible with index signature.
    Type 'string | null' is not assignable to type 'string | number | boolean | undefined'.
      Type 'null' is not assignable to type 'string | number | boolean | undefined'.
```

---
meta:
  title: "The type you provided for 'Presence' is not a valid JSON object"
  parentTitle: "Error"
  description: "Your 'Presence' type is incorrect and needs to be updated"
---

## Why this error occurred

You have provided a custom `Presence` type for your application, but the type
you provided isn’t a (completely) valid JSON object. Values like `Date`, `Map`,
`Set`, functions, classes, or `unknown` aren’t valid JSON.

```ts highlight="4-7"
declare global {
  interface Liveblocks {
    Presence: {
      // Your own fields go here...
      cursor: { x: number; y: number } | null;
      selection: string[];
      lastActivity: Date; // ❌ The issue is here
    };
  }
}
```

In the example above, the problem is in the `Date` field, because a `Date` isn’t
a valid JSON value.

## How to fix it

You’ll need to figure out what part of your provided `Presence` type definition
isn’t valid JSON. Sometimes this is immediately obvious, like in the example
above.

Sometimes the issue may be a bit less obvious:

```ts highlight="6-7"
import type { Json } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    Presence: {
      cursor: unknown; // ❌ Unknowns could contain non-JSON
      cursor: Json; // ✅ Prefer using Json
    };
  }
}
```

By using `Json`, you can still work with unknown or unspecified values, but
still ensure they will be valid JSON.

## If you cannot find the root cause

Sometimes types can be complex and the root cause is still unclear. In those
cases, there is a small trick you can use. Try to assign your type to the
required base type, with this line:

```ts highlight="9-10"
import type { JsonObject } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    Presence: MyPresence;
  }
}

// Quick debugging snippet to find root cause
const xxx: JsonObject = {} as MyPresence;
//    ^?
//    The error will appear here
```

Now TypeScript will explain why it thinks your type isn’t valid JSON:

```error showLineNumbers={false}
Type 'MyPresence' is not assignable to type 'JsonObject'.
 Property 'lastActivity' is incompatible with index signature.
   Type 'Date' is not assignable to type 'Json | undefined'.
     Type 'Date' is not assignable to type 'JsonObject'.
       Index signature for type 'string' is missing in type 'Date'.
```

---
meta:
  title: "The type you provided for 'RoomEvent' is not a valid JSON value"
  parentTitle: "Error"
  description: "Your 'RoomEvent' type is incorrect and needs to be updated"
---

## Why this error occurred

You have provided a custom `RoomEvent` type for your application, but the type
you provided isn’t a (completely) valid JSON value. Values like `Date`, `Map`,
`Set`, functions, classes, or `unknown` aren’t valid JSON.

For example, suppose you have defined three broadcastable events for your
application:

```ts highlight="4-7"
declare global {
  interface Liveblocks {
    RoomEvent:
      | { type: "beep" }
      | { type: "boop"; target: HtmlElement }
      //                        ^^^^^^^^^^^ ❌ The issue is here
      | { type: "buzz"; volume: number };
  }
}
```

In the example above, the problem is in the `HTMLElement` field, because a
`HTMLElement` isn’t a valid JSON value.

## How to fix it

You’ll need to figure out what part of your provided `RoomEvent` type definition
isn’t valid JSON. Sometimes this is immediately obvious, but sometimes the issue
may be a bit less obvious.

## If you cannot find the root cause

Sometimes types can be complex and the root cause is still unclear. In those
cases, there is a small trick you can use. Try to assign your type to the
required base type, with this line:

```ts highlight="9-10"
import type { Json } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    RoomEvent: MyRoomEvent;
  }
}

// Quick debugging snippet to find root cause
const xxx: Json = {} as MyRoomEvent;
//    ^?
//    The error will appear here
```

Now TypeScript will explain why it thinks your type isn’t valid JSON:

```error showLineNumbers={false}
Type 'MyRoomEvent' is not assignable to type 'Json'.
  Type '{ type: "boop"; target: HTMLElement; }' is not assignable to type 'Json'.
    Type '{ type: "boop"; target: HTMLElement; }' is not assignable to type 'JsonObject'.
      Property 'target' is incompatible with index signature.
        Type 'HTMLElement' is not assignable to type 'Json | undefined'.
          Type 'HTMLElement' is not assignable to type 'JsonObject'.
            Index signature for type 'string' is missing in type 'HTMLElement'.
```

---
meta:
  title: "The type you provided for 'RoomInfo' does not match its requirements"
  parentTitle: "Error"
  description: "Your 'RoomInfo' type is incorrect and needs to be updated"
---

## Why this error occurred

You have provided a custom `RoomInfo` type for your application, but the type
you provided isn’t a (completely) valid definition.

```ts highlight="4-10"
declare global {
  interface Liveblocks {
    RoomInfo: {
      // Your custom fields go here...
      name: string;
      url: URL; // ❌ The issue is here
      geo: {
        city: string;
        country: string;
      };
    };
  }
}
```

In the example above, the problem is the `URL` field. Values like `Date`, `Map`,
`Set`, functions, classes (including `URL`), or `unknown` aren’t valid JSON.

## The rules of the RoomInfo type

The following rules apply if you want to specify a custom `RoomInfo` type:

- You can provide any keys and values here, as long as the values are valid
  JSON.
- Two small constraints:
  - _If_ you specify `name`, it _must_ be assignable to `string | undefined`.
  - _If_ you specify `url`, then it _must_ be assignable to
    `string | undefined`.

The reason for these two restrictions is that some of our higher-level
components will pick these values up and use them to provide default UIs.

## How to fix it

You’ll need to figure out what part of your provided `RoomInfo` type definition
isn’t valid. The example above can be fixed as follows:

```tsx
declare global {
  interface Liveblocks {
    RoomInfo: {
      // Your custom fields go here...
      name: string;
      url: string; // ✅ Valid
      geo: {
        city: string;
        country: string;
      };
    };
  }
}
```

## If you cannot find the root cause

Sometimes types can be complex and the root cause is still unclear. In those
cases, there is a small trick you can use. Try to assign your type to the
required base type, with this line:

```ts highlight="9-10"
import type { BaseRoomInfo } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    RoomInfo: MyRoomInfo;
  }
}

// Quick debugging snippet to find root cause
const xxx: BaseRoomInfo = {} as MyRoomInfo;
//    ^?
//    The error will appear here
```

Now TypeScript will explain why it thinks your type isn’t valid room info:

```error showLineNumbers={false}
Type 'MyRoomInfo' is not assignable to type 'BaseRoomInfo'.
  Types of property 'url' are incompatible.
    Type 'URL' is not assignable to type 'string'.
```

---
meta:
  title: "The type you provided for 'Storage' is not a valid LSON value"
  parentTitle: "Error"
  description: "Your 'Storage' type is incorrect and needs to be updated"
---

## Why this error occurred

You have provided a custom `Storage` type for your application, but the type you
provided isn’t a (completely) valid LSON object. Values like `Date`, `Map`,
`Set`, functions, classes, or `unknown` aren’t valid LSON.

<Banner type="success" title="What was LSON again?">

LSON is either a valid JSON value, or an instance of `LiveMap`, `LiveList`, or
`LiveObject`.

</Banner>

```ts highlight="6"
declare global {
  interface Liveblocks {
    Storage: {
      layers: LiveMap<string, LiveObject<Layer>>;
      layerIds: LiveList<string>;
      createdAt: Date; // ❌ The issue is here
    };
  }
}
```

In the example above, the problem is in the `Date` field, because a `Date` isn’t
a valid LSON value.

## How to fix it

You’ll need to figure out what part of your provided `Storage` type definition
isn’t valid LSON. Sometimes this is immediately obvious, like in the example
above.

Sometimes the issue may be a bit less obvious:

```ts highlight="7-8"
import type { Lson } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    Storage: {
      layers: LiveMap<string, LiveObject<Layer>>;
      layerIds: LiveList<unknown>; // ❌ Unknowns could contain non-LSON
      layerIds: LiveList<Lson>; // ✅ Prefer using Lson
    };
  }
}
```

By using `Lson`, you can still work with unknown or unspecified values, but
still ensure they will be valid LSON.

## If you cannot find the root cause

Sometimes types can be complex and the root cause is still unclear. In those
cases, there is a small trick you can use. Try to assign your type to the
required base type, with this line:

```ts highlight="9-10"
import type { LsonObject } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    Storage: MyStorage;
  }
}

// Quick debugging snippet to find root cause
const xxx: LsonObject = {} as MyStorage;
//    ^?
//    The error will appear here
```

Now TypeScript will explain why it thinks your type isn’t valid LSON:

```error showLineNumbers={false}
Type 'MyStorage' is not assignable to type 'LsonObject'.
  Property 'layerIds' is incompatible with index signature.
    Type 'LiveList<unknown>' is not assignable to type 'Lson | undefined'.
      Type 'LiveList<unknown>' is not assignable to type 'LiveList<Lson>'.
        The types returned by 'toArray()' are incompatible between these types.
          Type 'unknown[]' is not assignable to type 'Lson[]'.
            Type 'unknown' is not assignable to type 'Lson'.
```

---
meta:
  title:
    "The type you provided for 'ThreadMetadata' does not match its requirements"
  parentTitle: "Error"
  description: "Your 'ThreadMetadata' type is incorrect and needs to be updated"
---

## Why this error occurred

You have provided a custom `ThreadMetadata` type for your application, but the
type you provided isn’t a (completely) valid definition.

```ts highlight="4-8"
declare global {
  interface Liveblocks {
    ThreadMetadata: {
      // Your custom fields go here...
      pinned: boolean;
      color: string | null; // ❌ Values may not be null
      position: { x: number; y: number }; // ❌ Values must be simple
      zIndex?: number;
    };
  }
}
```

In the example above, there are two problems. Thread metadata may only contain
simple key/value pairs, where the values must always be assignable to
`string | number | boolean | undefined`.

## How to fix it

You’ll need to figure out what part of your provided `ThreadMetadata` type
definition isn’t valid. The example above could be fixed as such:

```ts highlight="6-8"
declare global {
  interface Liveblocks {
    ThreadMetadata: {
      // Your custom fields go here...
      pinned: boolean;
      color?: string; // ✅
      positionX: number; // ✅
      positionY: number; // ✅
      zIndex?: number;
    };
  }
}
```

## If you cannot find the root cause

Sometimes types can be complex and the root cause is still unclear. In those
cases, there is a small trick you can use. Try to assign your type to the
required base type, with this line:

```ts highlight="9-10"
import type { BaseMetadata } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    ThreadMetadata: MyThreadMetadata;
  }
}

// Quick debugging snippet to find root cause
const xxx: BaseMetadata = {} as MyThreadMetadata;
//    ^?
//    The error will appear here
```

Now TypeScript will explain why it thinks your type isn’t valid metadata:

```error showLineNumbers={false}
Type 'MyThreadMetadata' is not assignable to type 'BaseMetadata'.
  Property 'color' is incompatible with index signature.
    Type 'string | null' is not assignable to type 'string | number | boolean | undefined'.
      Type 'null' is not assignable to type 'string | number | boolean | undefined'.
```

---
meta:
  title: "The type you provided for 'UserMeta' does not match its requirements"
  parentTitle: "Error"
  description: "Your 'UserMeta' type is incorrect and needs to be updated"
---

## Why this error occurred

You have provided a custom `UserMeta` type for your application, but the type
you provided isn’t a (completely) valid definition.

```ts highlight="4-10"
declare global {
  interface Liveblocks {
    UserMeta: {
      id: string;
      info: {
        name: string;
        color: string;
        picture: string;
        lastLogin: Date; // ❌ The issue is here
      };
    };
  }
}
```

In the example above, the problem is in the `Date` field. Values like `Date`,
`Map`, `Set`, functions, classes, or `unknown` aren’t valid JSON.

## The rules of the UserMeta type

The following rules apply if you want to specify a custom `UserMeta` type:

- Top-level fields `id` and `info` are special. They are optional, but only
  these two fields can exist. Extra fields you specify will not have any effect
  and will be ignored.
- _If_ you specify `id`, it _must_ still be assignable to `string | undefined`.
- _If_ you specify `info`, then it _must_ be a valid JSON object value.
  Furthermore:
  - _If_ you specify a `name` field inside `info`, it _must_ still be assignable
    to `string | undefined`.
  - _If_ you specify a `avatar` field inside `info`, it _must_ still be
    assignable to `string | undefined`.

The reason for the last two restrictions is that some of our higher-level
components will pick these values up and use them to provide default UIs.

```tsx
declare global {
  interface Liveblocks {
    UserMeta: {
      id: string; // ✅ Valid
      id?: string; // ✅ Valid
      id?: number; // ❌ Invalid, not assignable to string | undefined

      info: {
        name: string; // ✅ Valid
        name?: string; // ✅ Valid
        name: number; // ❌ Invalid, not assignable to string | undefined

        avatar: string; // ✅ Valid
        avatar?: string; // ✅ Valid
        avatar: number; // ❌ Invalid, not assignable to string | undefined

        // Other field names are not special and are free-form
        custom: string; // ✅ Valid
        custom?: string; // ✅ Valid
        custom: number; // ✅ Valid
        custom: Json; // ✅ Valid, can take _any_ valid Json value

        // However, they still need to be valid JSON
        custom: unknown; // ❌ Invalid, not value JSON
      };
    };
  }
}
```

Also please note:

```tsx
declare global {
  interface Liveblocks {
    UserMeta: {
      id: string;
      info: { name: string };

      // Only `id` or `info` make sense inside `UserMeta`
      iWillBeIgnored: string;
    };
  }
}
```

## How to fix it

You’ll need to figure out what part of your provided `UserMeta` type definition
isn’t valid. Sometimes this is immediately obvious, but sometimes the issue may
be a bit less obvious.

## If you cannot find the root cause

Sometimes types can be complex and the root cause is still unclear. In those
cases, there is a small trick you can use. Try to assign your type to the
required base type, with this line:

```ts highlight="9-10"
import type { BaseUserMeta } from "@liveblocks/client";

declare global {
  interface Liveblocks {
    UserMeta: MyUserMeta;
  }
}

// Quick debugging snippet to find root cause
const xxx: BaseUserMeta = {} as MyUserMeta;
//    ^?
//    The error will appear here
```

Now TypeScript will explain why it thinks your type isn’t valid JSON:

```error showLineNumbers={false}
Type 'MyUserMeta' is not assignable to type 'BaseUserMeta'.
  Types of property 'info' are incompatible.
    Type '{ name: string; color: string; picture: string; lastLogin: Date; }' is not assignable to type 'IUserInfo'.
      Property 'lastLogin' is incompatible with index signature.
        Type 'Date' is not assignable to type 'Json | undefined'.
          Type 'Date' is not assignable to type 'JsonObject'.
            Index signature for type 'string' is missing in type 'Date'.
```

---
meta:
  title: "Cross-linked Liveblocks versions are found in your project"
  parentTitle: "Error"
  description:
    "Cross-linked Liveblocks versions are found in your project. This will cause
    issues!"
---

## Why this error occurred

You’re using multiple `@liveblocks/*` packages in your application bundle, but
they’re not all on the same version.

For example: you’re using `@liveblocks/zustand` at 1.1.3, but
`@liveblocks/react` is on 1.1.4.

## How to find the culprit?

To find out if your project is affected, you can run

```bash
npm ls | grep @liveblocks
```

Please make sure that all of the versions listed there are on the same version.

## Possible ways to fix it

When you upgrade one Liveblocks package, make sure to also upgrade other
Liveblocks packages to the same version. The easiest way to upgrade all your
Liveblocks packages to the same version is:

- Upgrades to the latest version:
  ```bash
  npx create-liveblocks-app@latest --upgrade
  ```
- Upgrades to a specific version:
  ```bash
  npx create-liveblocks-app@latest --upgrade=3.13.3
  ```

---
meta:
  title: "Multiple copies of Liveblocks are being loaded in your project"
  parentTitle: "Error"
  description:
    "Multiple copies of Liveblocks are being loaded in your project. This will
    cause issues!"
---

## Why this error occurred

Multiple copies of some Liveblocks package ended up being bundled in your
application bundle.

This can happen because your production bundle includes two entire copies of two
different versions of Liveblocks. Or it can be the case that it includes the
same version of Liveblocks in the bundle twice because the ESM and the CJS
version got bundled separately.

It’s important that only a single copy of Liveblocks exists in your application
bundle at runtime, otherwise bugs will happen. Plus your bundle will be
unnecessarily large.

## Possible causes

- Your project is using
  [internal packages](#using-liveblocks-with-internal-packages) that also rely
  on Liveblocks, but maybe some are on different versions.
- Your project is using a non-standard bundler setup. If you believe this is an
  issue with the way Liveblocks is packaged, feel free to open a support
  request.

## Possible ways to fix it

To investigate your setup, run the following command to see if all your
Liveblocks dependencies are on the same version:

```bash
npm ls | grep @liveblocks
```

If they're not all on the same version, you can upgrade them by running:

```bash
npx create-liveblocks-app@latest --upgrade
```

If you're using shared internal packages that depend on Liveblocks, see
[Using Liveblocks with internal packages](#using-liveblocks-with-internal-packages)
below for the recommended setup.

If all your Liveblocks dependencies are on the same version already, and you're
still seeing this error, you’re experiencing the
[dual-package hazard](https://nodejs.org/api/packages.html#dual-package-hazard)
problem, which means that both the ESM and the CJS version of Liveblocks end up
in your production bundle as two separate "instances". Of course, this isn’t
supposed to happen. Please let us know about this by
[opening a GitHub issue](https://github.com/liveblocks/liveblocks/issues/new?template=bug_report.md),
or reaching out in our support channel on
[Discord](https://liveblocks.io/discord).

## Using Liveblocks with internal packages

Consider a setup where you have two applications that both depend on Liveblocks
directly, and on a shared internal package that also depends on Liveblocks.

If the shared package declares Liveblocks as a regular dependency, your package
manager will install the Liveblocks version specified by the shared package,
which may conflict with the version your application needs:

![Do not pin the Liveblocks dependency in your shared library as it can lead to conflicts](/assets/errors/dont-use-normal-dependencies.png)
<small style={{ display: "block", textAlign: "center" }}>The version numbers used here are just examples.</small>

The recommended approach is to declare Liveblocks as a "peer dependency" in your
shared package's `package.json`. This tells the package manager: "I'm compatible
with Liveblocks ^3, but the host application decides which version to use."
Then, only declare Liveblocks in the `"dependencies"` of your actual
applications.

```json
{
  "peerDependencies": {
    "@liveblocks/client": "^3",
    "@liveblocks/react": "^3"
  }
}
```

Use a major version range like `"^3"` to allow flexibility while ensuring
compatibility.

![Using a peer dependency on Liveblocks from your shared library helps to avoid version conflicts](/assets/errors/use-peer-dependencies.png)
<small style={{ display: "block", textAlign: "center" }}>The version numbers used here are just examples.</small>

---
meta:
  title: "In order to use this channel, please set up your project first"
  parentTitle: "Error"
  description:
    "A notification channel hasn’t been set up, you need to enable it in your
    dashboard."
---

## Why this error occurred

You tried to access a notification channel, such as `settings.email`, but it’s
disabled in your Liveblocks dashboard.

## How to fix it

You first need to navigate to your project in the [dashboard](/dashboard).

<Figure>
  <Image
    src="/assets/tutorials/webhooks/liveblocks-click-notifications.png"
    alt="Notifications dashboard page"
    width={712}
    height={445}
  />
</Figure>

Then, enable a notification kind on the channel you’re trying to access. For
example, if you’re trying to access `settings.email`, navigate to the _Email_
tab and enable a notification kind.

<Banner type="warning">

If you’re working with a production app, make sure you
[read our guide on enabling notification kinds](/docs/guides/what-to-check-before-enabling-a-new-notification-kind)
before publishing your changes, as your app may be affected.

</Banner>

<Figure>
  <Image
    src="/assets/tutorials/webhooks/liveblocks-toggle-custom-notification.png"
    alt="Toggle a custom notification kind"
    width={712}
    height={445}
  />
</Figure>

Once a notification kind is enabled on the channel you’re accessing, the error
will disappear.

## Learn more

Learn more in our documentation.

- [What to check before enabling a new notification kind](/docs/guides/what-to-check-before-enabling-a-new-notification-kind).
- [How to create a notification settings panel](/docs/guides/how-to-create-a-notification-settings-panel).
- [`useNotificationSettings`](/docs/api-reference/liveblocks-react#useNotificationSettings)
- [`client.getNotificationSettings`](/docs/api-reference/liveblocks-client#Client.getNotificationSettings)
- [`Liveblocks.getNotificationSettings`](/docs/api-reference/liveblocks-node#get-users-userId-notification-settings)

---
meta:
  title: "The issued access token doesn't grant enough permissions."
  parentTitle: "Error"
  description:
    "@liveblocks/client error: the issued access token doesn't grant enough
    permissions"
---

## Why this error occurred

The client may request an access token to access resources beyond a specific
room, like Notifications. In such cases, your auth endpoint should issue an
access token that grants access to multiple rooms using wildcards (`*`).

### 1.10 warning

If you saw this warning in your auth endpoint, it's because the version 1.10
allows you to create an access token with no permission, even though it's not
recommended. When doing so, Notifications will work with an access token without
permissions, and Liveblocks will return all the user's notifications (mentions
for example) even if the user doesn't have access to the room. You should move
to use wildcards or use ID tokens, and note that this will not be allowed in
later versions.

## Possible ways to fix it

In your auth endpoint, when the request's property `room` is undefined, you
should create a token that grants access to multiple rooms. Example:

```
session.allow("orga1*", session.FULL_ACCESS);
```

Learn more about
[`permissions with access tokens`](https://liveblocks.io/docs/authentication/access-token).

---
meta:
  title: "You need to polyfill atob to use the client in your environment"
  parentTitle: "Error"
  description:
    "@liveblocks/client error: atob polyfill is required in your environment."
---

## Why this error occurred

You are using `@liveblocks/client` within a JavaScript runtime which doesn't
implement [`atob`](https://developer.mozilla.org/en-US/docs/Web/API/atob).
(environments like React Native or Node < 16)

## Possible ways to fix it

As a polyfill, we recommend installing the package
[`base-64`](https://www.npmjs.com/package/base-64).

```bash
npm install base-64
```

Then you can pass the `decode` function to our `atob` polyfill option when you
create the client.

```ts highlight="1,5-7" file="src/index.js"
import { decode } from "base-64";

const client = createClient({
  /* ... your other client's options */
  polyfills: {
    atob: decode,
  },
});
```

---
meta:
  title: "RoomProvider id property is required"
  parentTitle: "Error"
  description: "@liveblocks/react error: RoomProvider id property is required."
---

## Why this error occurred

[`RoomProvider`][] `id` property is required in order to provide a [`Room`][] in
the tree below.

## Possible ways to fix it

```jsx highlight="5"
import { RoomProvider } from "@liveblocks/react";

function Component() {
  return (
    <RoomProvider id="your-room-id">
      <YourComponent />
    </RoomProvider>
  );
}
```

Sometimes, you don’t have access to your room `id` right away. For example, if
you’re using Next.js and the `id` is coming from the query string, it’s easy to
forget that `useRouter().query.roomId` returns `undefined` on the first render.
Do not render the [`RoomProvider`][] if you don’t have access to the room `id`
right away.

## Useful links

- [`RoomProvider`][]
- [`Room`][]

[`roomprovider`]: /docs/api-reference/liveblocks-react#RoomProvider
[`room`]: /docs/api-reference/liveblocks-client#Room

---
meta:
  title: "Get started with Liveblocks and JavaScript"
  parentTitle: "Quickstart"
  description: "Learn how to get started with Liveblocks and JavaScript"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your JavaScript application using the APIs from the
[`@liveblocks/client`](/docs/api-reference/liveblocks-client) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      The first step in connecting to Liveblocks is creating a client which
      will be responsible for communicating with the back end.

      ```ts file="room.js"
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people collaborate.
      To create a realtime experience, multiple users must be connected to the same room.

      ```js file="room.js"
      const { room, leave } = client.enterRoom("my-room");
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Use the Liveblocks methods</StepTitle>
    <StepContent>

      Now that we’re connected to a room, we can start using Liveblocks subscriptions.
      The first we’ll add is `others`, a subscription that provides information
      about which other users are connected to the room.

      ```js file="index.js" highlight="5-7"
      import { room } from "./room.js"

      const div = document.querySelector("div");

      room.subscribe("others", (others) => {
        div.innerText = `There are ${others.length} other user(s) online`;
      });
      ```

    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>

      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation to start building
collaborative experiences for your JavaScript application.

- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [JavaScript guides](/docs/guides?technologies=javascript)
- [Authentication](/docs/authentication)

---

## Examples using JavaScript

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative To-do List",
      slug: "collaborative-todo-list/javascript-todo-list",
      image: "/images/examples/thumbnails/collaborative-todo-list.jpg",
    }}
    technologies={["nextjs", "vuejs", "sveltekit", "solidjs", "javascript"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Live Cursors",
      slug: "live-cursors/javascript-live-cursors",
      image: "/images/examples/thumbnails/live-avatar-stack.jpg",
    }}
    technologies={["nextjs", "vuejs", "sveltekit", "solidjs", "javascript"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with an AI chat using Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to get started with an AI chat using Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding an
advanced AI chat to your Next.js `/app` directory application using the hooks
from [`@liveblocks/react`](/docs/api-reference/liveblocks-react) and the
components from
[`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add your project’s secret key</StepTitle>
    <StepContent>

      Create a new `.env.local` file and add your Liveblocks secret key from the [dashboard](/dashboard/apikeys).

      ```env file=".env.local"
      LIVEBLOCKS_SECRET_KEY="{{SECRET_KEY}}"
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up authentication</StepTitle>
    <StepContent>

      Create an [authentication](/docs/authentication) API route with [`prepareSession`](/docs/api-reference/liveblocks-node#prepareSession),
      passing a unique user ID.
      Each user has their own set of private chats, and this allows Liveblocks to identify who is connecting.

      ```tsx file="app/api/liveblocks-auth/route.ts"
      import { Liveblocks } from "@liveblocks/node";
      import { NextRequest } from "next/server";

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

      export async function POST(req: NextRequest) {
        // Get the current user from your database
        const user = {
          // +++
          id: "user-1",
          // +++
          info: {
            // Optional. `name`, `avatar`, `color`, custom info.
          },
        };

        // Create a session for the current user
        // +++
        const session = liveblocks.prepareSession(user.id, {
          userInfo: user.info,
        });
        // +++

        // Give the user access to AI chats
        session.allow(`*`, session.FULL_ACCESS);

        // Authorize the user and return the result
        const { status, body } = await session.authorize();
        return new Response(body, { status });
      }
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Add your secret key</StepTitle>
    <StepContent>
      Create a project in the [dashboard](/dashboard) and add your secret key to `.env.local`:

    ```js file=".env.local"
    LIVEBLOCKS_SECRET_KEY="{{SECRET_KEY}}"
    ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Create a Liveblocks provider</StepTitle>
    <StepContent>

      To connect the user to their chats, you must
      add [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
      to a client component in your app.

      ```tsx file="app/Providers.tsx" highlight="8-10"
      "use client";

      import { ReactNode } from "react";
      import { LiveblocksProvider } from "@liveblocks/react";

      export function Providers({ children }: { children: ReactNode }) {
        return (
          <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
            {children}
          </LiveblocksProvider>
        );
      }
    ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add the provider to your layout</StepTitle>
    <StepContent>

      After creating your provider file, it’s time to use it. Import
      your room into your `layout.tsx` file.

      ```tsx file="app/layout.tsx"
      import { Providers } from "./Providers";

      export default function Layout({ children }) {
        return (
          <html>
            <body>
              // +++
              <Providers>
                {children}
              </Providers>
              // +++
            </body>
          </html>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add the AI chat component</StepTitle>
    <StepContent>

      Now you can add the [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat)
      component to your application. This component renders a fully-featured AI
      chat interface with message history and a composer. You can add
      [tools](/docs/ready-made-features/ai-copilots/tools), [knowledge](/docs/ready-made-features/ai-copilots/knowledge),
      and use [hooks](/docs/ready-made-features/ai-copilots/hooks).

      ```tsx file="app/page.tsx"
      "use client";

      import { AiChat } from "@liveblocks/react-ui";
      import { RegisterAiTool, RegisterAiKnowledge } from "@liveblocks/react";

      export default function Page() {
        return (
          <div style={{ height: 500, width: 500 }}>
            // +++
            <AiChat
              chatId="my-first-chat"
              // copilotId="co_aH1j2..."
            />
            // +++
            {/* <RegisterAiTool ... /> and <RegisterAiKnowledge ... /> */}
          </div>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Import default styles</StepTitle>
    <StepContent>

    The default components come with default styles, you can import them into the
    root layout of your app or directly into a CSS file with `@import`.

    ```tsx file="app/layout.tsx"
    import "@liveblocks/react-ui/styles.css";
    ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: create a copilot</StepTitle>
    <StepContent>

      AI Copilots is set up and working, but it’s using our default AI model
      and prompt—the next step is to create a custom copilot
      using your preferred AI provider and configuration.

      <Button asChild  className="not-markdown">
        <a href="/docs/ready-made-features/ai-copilots/copilots">
          Create a copilot
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You’ve set up the foundation to start building an AI copilot
experience for your Next.js application.

- [Overview of all features and where to start](/docs/ready-made-features/ai-copilots)
- [@liveblocks/react API reference](/docs/api-reference/liveblocks-react#AI-Copilots)
- [@liveblocks/react-ui API reference](/docs/api-reference/liveblocks-react-ui#AI-Copilots)
- [How to use a fallback AI model](/docs/guides/how-to-use-fallback-ai-models-in-ai-copilots)

---

## Examples using Next.js

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "AI Chats",
      slug: "ai-chats/nextjs-ai-chats",
      image: "/images/examples/thumbnails/ai-chats.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "AI Reports Dashboard",
      slug: "ai-dashboard-reports/nextjs-ai-dashboard-reports",
      image: "/images/examples/thumbnails/ai-reports-dashboard.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "AI Support Chat",
      slug: "ai-support-chat/nextjs-ai-support-chat",
      image: "/images/examples/thumbnails/ai-support.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "AI App Builder",
      slug: "ai-app-builder/nextjs-ai-app-builder",
      image: "/images/examples/thumbnails/ai-app-builder.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with a BlockNote text editor using Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to get started with a BlockNote text editor using Liveblocks and
    Next.js."
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Next.js application using the APIs from the
[`@liveblocks/react-blocknote`](/docs/api-reference/liveblocks-react-blocknote)
package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks and BlockNote</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/react-blocknote @blocknote/core @blocknote/react @blocknote/mantine
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Create a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. When using Next.js’ `/app` router,
      we recommend creating your room in a `Room.tsx` file in the same directory
      as your current route.

      Set up a Liveblocks client with
      [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider),
      join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider),
      and use [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense)
      to add a loading spinner to your app.

      ```tsx file="app/Room.tsx" highlight="12-18"
      "use client";

      import { ReactNode } from "react";
      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";

      export function Room({ children }: { children: ReactNode }) {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                {children}
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

  </StepContent>

</Step>
<Step>
  <StepTitle>Add the Liveblocks room to your page</StepTitle>
  <StepContent>

    After creating your room file, it’s time to join it. Import
    your room into your `page.tsx` file, and place
    your collaborative app components inside it.

    ```tsx file="app/page.tsx" highlight="6-8"
    import { Room } from "./Room";
    import { Editor } from "./Editor";

    export default function Page() {
      return (
        <Room>
          <Editor/>
        </Room>
      );
    }
    ```

  </StepContent>

</Step>
<Step>
  <StepTitle>Set up the collaborative BlockNote text editor</StepTitle>
  <StepContent>

    Now that we set up Liveblocks, we can start integrating BlockNote and Liveblocks in the `Editor.tsx` file.
    To make the editor collaborative, we can add
    [`useCreateBlockNoteWithLiveblocks`](/docs/api-reference/liveblocks-react-blocknote#useCreateBlockNoteWithLiveblocks)
    from `@liveblocks/react-blocknote`.

    ```tsx file="app/Editor.tsx"
    "use client";

    import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";
    import { BlockNoteView } from "@blocknote/mantine";
    import { Threads } from "./Threads";

    export function Editor() {
      const editor = useCreateBlockNoteWithLiveblocks({});

      return (
        <div>
          <BlockNoteView editor={editor} className="editor" />
          <Threads editor={editor} />
        </div>
      );
    }
    ```

</StepContent>

</Step>
<Step>
  <StepTitle>Render threads and composer</StepTitle>
  <StepContent>

    To add [Comments](/docs/ready-made-features/comments) to your text editor, we need to import a thread composer
    and list each thread on the page. Create a `Threads.tsx` file that uses [`FloatingComposer`](/docs/api-reference/liveblocks-react-blocknote#FloatingComposer)
    for creating new threads, alongside [`AnchoredThreads`](/docs/api-reference/liveblocks-react-blocknote#AnchoredThreads) and [`FloatingThreads`](/docs/api-reference/liveblocks-react-blocknote#FloatingThreads) for displaying threads on desktop and mobile.

    ```tsx file="app/Threads.tsx"
    import { useThreads } from "@liveblocks/react/suspense";
    import {
      AnchoredThreads,
      FloatingComposer,
      FloatingThreads,
    } from "@liveblocks/react-blocknote";
    import { BlockNoteEditor } from "@blocknote/core";

    export function Threads({ editor }: { editor: BlockNoteEditor | null }) {
      const { threads } = useThreads({ query: { resolved: false } });

      if (!editor) {
        return null;
      }

      return (
        <>
          <div className="anchored-threads">
            <AnchoredThreads editor={editor} threads={threads} />
          </div>
          <FloatingThreads
            editor={editor}
            threads={threads}
            className="floating-threads"
          />
          <FloatingComposer editor={editor} className="floating-composer" />
        </>
      );
    }
    ```

</StepContent>

</Step>
<Step>
  <StepTitle>Style your editor</StepTitle>
  <StepContent>

    We can create some custom styles to position the editor and threads—import `globals.css`, alongside the default BlockNote and Liveblocks styles. You can import them into the
    root layout of your app or directly into a CSS file with `@import`.

    ```css file="app/globals.css" isCollapsed isCollapsable
    html {
      font-family: Inter, sans-serif;
      background: #f9f9f9;
    }

    @media (prefers-color-scheme: dark) {
      html {
        background: #0c0c0c;
      }
    }

    .editor {
      position: absolute;
      inset: 0;
      max-width: 1024px;
      margin: 0 auto;
      padding: 48px 0;
    }

    .bn-editor {
      padding: 36px 52px;
      min-height: 100%;
    }

    /* For mobile */
    .floating-threads {
      display: none;
    }

    /* For desktop */
    .anchored-threads {
      display: block;
      max-width: 300px;
      width: 100%;
      position: absolute;
      right: 12px;
    }

    @media (max-width: 640px) {
      .floating-threads {
        display: block;
      }

      .anchored-threads {
        display: none;
      }
    }
    ```

    ```tsx file="app/layout.tsx"
    import "@blocknote/core/fonts/inter.css";
    import "@blocknote/mantine/style.css";
    import "@liveblocks/react-ui/styles.css";
    import "@liveblocks/react-ui/styles/dark/media-query.css";
    import "@liveblocks/react-tiptap/styles.css";
    import "./globals.css";
    ```

  </StepContent>

</Step>
<Step>
  <StepTitle>Next: authenticate and add your users</StepTitle>
  <StepContent>
    Text Editor is set up and working now, but each user is anonymous—the next step is to
    authenticate each user as they connect, and attach their name, color, and avatar, to their cursors and mentions.

    <Button asChild  className="not-markdown">
      <a href="/docs/guides/how-to-add-users-to-liveblocks-text-editor">
        Add your users to Text Editor
      </a>
    </Button>

  </StepContent>

</Step>
<Step lastStep>
  <StepTitle>Optional: add more features</StepTitle>
  <StepContent>
    BlockNote is easy to extend, and a number of extensions are already available, making it possible to quickly create complex rich-text applications. For example you can enable features
    such as tables, text highlights, embedded images, and more. This is all supported using Liveblocks.

    <Button asChild  className="not-markdown">
      <a href="https://tiptap.dev/docs/editor/extensions/overview">
        Find more extensions
      </a>
    </Button>

  </StepContent>

</Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
BlockNote text editor inside your React application.

- [@liveblocks/react-blocknote API Reference](/docs/api-reference/liveblocks-react-blocknote)
- [BlockNote guides](/docs/guides?technologies=blocknote)
- [BlockNote website](https://www.blocknotejs.org/)

---

## Examples using BlockNote

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Text Editor",
      slug: "collaborative-text-editor/nextjs-blocknote",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with commenting using Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to get started with commenting using Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding a
commenting experience to your Next.js `/app` directory application using the
hooks from [`@liveblocks/react`](/docs/api-reference/liveblocks-react) and the
components from
[`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Create a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. When using Next.js’ `/app` router,
      we recommend creating your room in a `Room.tsx` file in the same directory
      as your current route.

      Set up a Liveblocks client with
      [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider),
      join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider),
      and use [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense)
      to add a loading spinner to your app.

      ```tsx file="app/Room.tsx" highlight="12-18"
      "use client";

      import { ReactNode } from "react";
      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";

      export function Room({ children }: { children: ReactNode }) {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                {children}
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add the Liveblocks room to your page</StepTitle>
    <StepContent>

      After creating your room file, it’s time to join it. Import
      your room into your `page.tsx` file, and place
      your collaborative app components inside it.

      ```tsx file="app/page.tsx" highlight="6-8"
      import { Room } from "./Room";
      import { CollaborativeApp } from "./CollaborativeApp";

      export default function Page() {
        return (
          <Room>
            <CollaborativeApp />
          </Room>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Use the Liveblocks hooks and components</StepTitle>
    <StepContent>

      Now that we’re connected to a room, we can start using the Liveblocks hooks and components.
      We’ll add [`useThreads`](/docs/api-reference/liveblocks-react#useThreads) to get the threads in the room, then we’ll use the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component
      to render them. Finally, we’ll add a way to create threads by adding a [`Composer`](/docs/api-reference/liveblocks-react-ui#Composer).

      ```tsx file="app/CollaborativeApp.tsx" highlight="7,11-14"
      "use client";

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

      export function CollaborativeApp() {
        const { threads } = useThreads();

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

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Import default styles</StepTitle>
    <StepContent>

    The default components come with default styles, you can import them into the
    root layout of your app or directly into a CSS file with `@import`.

    ```tsx file="app/layout.tsx"
    import "@liveblocks/react-ui/styles.css";
    ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: authenticate and add your users</StepTitle>
    <StepContent>

      Comments is set up and working now, but each user is anonymous—the next step is to
      authenticate each user as they connect, and attach their name and avatar to their comments.

      <Button asChild  className="not-markdown">
        <a href="/docs/guides/how-to-add-users-to-liveblocks-comments">
          Add your users to Comments
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You’ve set up the foundation to start building a commenting
experience for your Next.js application.

- [API Reference](/docs/api-reference/liveblocks-react-ui)
- [How to send email notifications when comments are created](/docs/guides/how-to-send-email-notifications-when-comments-are-created)

---

## Examples using Next.js

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments",
      slug: "comments/nextjs-comments",
      image: "/images/examples/thumbnails/comments.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Video Comments",
      slug: "video-comments/nextjs-comments-video",
      image: "/images/examples/thumbnails/comments-video.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Overlay Comments",
      slug: "overlay-comments/nextjs-comments-overlay",
      image: "/images/examples/thumbnails/comments-overlay.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Text Editor Comments",
      slug: "text-editor-comments/nextjs-comments-tiptap",
      image: "/images/examples/thumbnails/comments-text-editor.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with a Lexical text editor using Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to get started with a Lexical text editor using Liveblocks and
    Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Next.js application using the APIs from the
[`@liveblocks/react-lexical`](/docs/api-reference/liveblocks-react-lexical)
package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks and Lexical</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/react-lexical lexical @lexical/react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Create a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. When using Next.js’ `/app` router,
      we recommend creating your room in a `Room.tsx` file in the same directory
      as your current route.

      Set up a Liveblocks client with
      [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider),
      join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider),
      and use [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense)
      to add a loading spinner to your app.

      ```tsx file="app/Room.tsx" highlight="12-18"
      "use client";

      import { ReactNode } from "react";
      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";

      export function Room({ children }: { children: ReactNode }) {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                {children}
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add the Liveblocks room to your page</StepTitle>
    <StepContent>

      After creating your room file, it’s time to join it. Import
      your room into your `page.tsx` file, and place
      your collaborative app components inside it.

      ```tsx file="app/page.tsx" highlight="6-8"
      import { Room } from "./Room";
      import { Editor } from "./Editor";

      export default function Page() {
        return (
          <Room>
            <Editor/>
          </Room>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the collaborative Lexical text editor</StepTitle>
    <StepContent>

      Now that we set up Liveblocks, we can start integrating Lexical and Liveblocks in the `Editor.tsx` file.
      To make the editor collaborative, we can use
      [`LiveblocksPlugin`](/docs/api-reference/liveblocks-react-lexical#LiveblocksPlugin)
      from `@liveblocks/react-lexical`. [`FloatingToolbar`](/docs/api-reference/liveblocks-react-lexical#FloatingToolbar)
      adds a text selection toolbar.

      ```tsx file="app/Editor.tsx"
      "use client";

      import { LexicalComposer } from "@lexical/react/LexicalComposer";
      import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
      import { ContentEditable } from "@lexical/react/LexicalContentEditable";
      import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
      import {
        liveblocksConfig,
        LiveblocksPlugin,
        FloatingToolbar,
      } from "@liveblocks/react-lexical";
      import { Threads } from "./Threads";

      export function Editor() {
        // Wrap your Lexical config with `liveblocksConfig`
        const initialConfig = liveblocksConfig({
          namespace: "Demo",
          onError: (error: unknown) => {
            console.error(error);
            throw error;
          },
        });

        return (
          <LexicalComposer initialConfig={initialConfig}>
            <div className="editor">
              <RichTextPlugin
                contentEditable={<ContentEditable />}
                placeholder={<div className="placeholder">Start typing here…</div>}
                ErrorBoundary={LexicalErrorBoundary}
              />
              <LiveblocksPlugin>
                <Threads />
                <FloatingToolbar />
              </LiveblocksPlugin>
            </div>
          </LexicalComposer>
        );
      }
      ```
    </StepContent>

  </Step>
  <Step>
    <StepTitle>Render threads and composer</StepTitle>
    <StepContent>

      To add [Comments](/docs/ready-made-features/comments) to your text editor, we need to import a thread composer
      and list each thread on the page. Create a `Threads.tsx` file that uses [`FloatingComposer`](/docs/api-reference/liveblocks-react-lexical#FloatingComposer)
      for creating new threads, alongside [`AnchoredThreads`](/docs/api-reference/liveblocks-react-lexical#AnchoredThreads) and [`FloatingThreads`](/docs/api-reference/liveblocks-react-lexical#FloatingThreads) for displaying threads on desktop and mobile.

      ```tsx file="app/Threads.tsx"
      "use client";

      import { useThreads } from "@liveblocks/react/suspense";
      import {
        AnchoredThreads,
        FloatingComposer,
        FloatingThreads,
      } from "@liveblocks/react-lexical";

      export function Threads() {
        const { threads } = useThreads();

        return (
          <>
            <div className="anchored-threads">
              <AnchoredThreads threads={threads} />
            </div>
            <FloatingThreads className="floating-threads" threads={threads} />
            <FloatingComposer className="floating-composer" />
          </>
        );
      }
      ```
    </StepContent>

  </Step>
  <Step>
    <StepTitle>Style your editor</StepTitle>
    <StepContent>

      Lexical text editor is unstyled by default, so we can create some custom styles for it in a `globals.css` file. Import `globals.css`, alongside the default Liveblocks styles. You can import them into the
      root layout of your app or directly into a CSS file with `@import`.

      ```css file="app/globals.css" isCollapsed isCollapsable
      .editor {
        position: relative;
        display: flex;
        width: 100%;
        height: 100%;
      }

      [data-lexical-editor] {
        padding: 2px 12px;
        outline: none;
      }

      [data-lexical-editor] p {
        margin: 0.8em 0;
      }

      /* For mobile */
      .floating-threads {
        display: none;
      }

      /* For desktop */
      .anchored-threads {
        display: block;
        max-width: 300px;
        width: 100%;
        position: absolute;
        right: 4px;
      }

      @media (max-width: 640px) {
        .floating-threads {
          display: block;
        }

        .anchored-threads {
          display: none;
        }
      }

      .placeholder {
        position: absolute;
        left: 12px;
        top: 16px;
        pointer-events: none;
        opacity: 0.5;
      }
      ```

      ```tsx file="app/layout.tsx"
      import "@liveblocks/react-ui/styles.css";
      import "@liveblocks/react-lexical/styles.css";
      import "./globals.css";
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Next: authenticate and add your users</StepTitle>
    <StepContent>
      Text Editor is set up and working now, but each user is anonymous—the next step is to
      authenticate each user as they connect, and attach their name, color, and avatar, to their cursors and mentions.

      <Button asChild  className="not-markdown">
        <a href="/docs/guides/how-to-add-users-to-liveblocks-text-editor">
          Add your users to Text Editor
        </a>
      </Button>
    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Optional: add more features</StepTitle>
    <StepContent>
      Lexical is a highly extensible text editor and it's possible to create complex rich-text applications.
      A great example is in the [Lexical playground](https://playground.lexical.dev/) which enables features
      such as tables, text highlights, embedded images, and more. This is all supported using Liveblocks.

      <Button asChild  className="not-markdown">
        <a href="https://lexical.dev/docs/intro">
          Learn more about Lexical
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Lexical text editor inside your Next.js application.

- [Overview](/docs/ready-made-features/text-editor/lexical)
- [`@liveblocks/react-lexical` API Reference](/docs/api-reference/liveblocks-react-lexical)
- [`@liveblocks/node-lexical` API Reference](/docs/api-reference/liveblocks-node-lexical)
- [Lexical website](https://lexical.dev/)

---

## Examples using Lexical

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Text Editor",
      slug: "collaborative-text-editor/nextjs-lexical",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with emailing custom notifications using Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to email custom notifications using using Liveblocks Liveblocks
    and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to send
[custom notifications](/docs/ready-made-features/notifications/concepts#Custom-notifications)
to email using methods from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks Node</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>

   <Step>
    <StepTitle>Trigger a custom notification</StepTitle>
    <StepContent>

      Trigger a custom notification using
      [`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger)
      from a server action or route handler. In this example, a custom `$fileUploaded` notification is sent.

      ```ts
      "use server";

      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      // Call this in your app to create a custom notification
      export async function triggerCustomNotification() {
        // +++
        await liveblocks.triggerInboxNotification({
          // The user that will receive the notification
          userId: "steven@example.com",
          kind: "$fileUploaded",
          subjectId: "my-file",

          // Custom data for this notification
          activityData: {
            file: "https://example.com/my-file.zip",
            status: "pending",
          },
        });
        // +++
      }
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Follow our guide</StepTitle>
    <StepContent>

      Follow our step-by-step guide to learn set up email notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-notify-users-about-unread-custom-notifications-outside-of-your-app">
          How to notify users with email
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You've set up Email notifications in your Next.js application.

- [Webhooks API reference](/docs/platform/webhooks)
- [Node.js API reference](/docs/api-reference/liveblocks-node)
- [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "https://liveblocks.io/examples/comments-emails",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with an in-app custom notification inbox using Liveblocks and
    Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to add an in-app custom notification inbox using using Liveblocks
    Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to start adding custom
notifications to your Next.js `/app` directory application using the hooks from
[`@liveblocks/react`](/docs/api-reference/liveblocks-react) and the components
from [`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Create a Liveblocks provider</StepTitle>
    <StepContent>

      Liveblocks Notifications uses the concept of projects, which relate to
      projects in [your dashboard](/dashboard). Notifications are sent between
      users in the same project. To connect and receive notifications, you must
      add [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
      to a client component in your app.

      ```tsx file="app/Providers.tsx" highlight="8-10"
      "use client";

      import { ReactNode } from "react";
      import { LiveblocksProvider } from "@liveblocks/react";

      export function Providers({ children }: { children: ReactNode }) {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            {children}
          </LiveblocksProvider>
        );
      }
    ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add the provider to your layout</StepTitle>
    <StepContent>

      After creating your provider file, it’s time to use it. Import
      your room into your `layout.tsx` file.

      ```tsx file="app/layout.tsx"
      import { Providers } from "./Providers";

      export default function Layout({ children }) {
        return (
          <html>
            <body>
              // +++
              <Providers>
                {children}
              </Providers>
              // +++
            </body>
          </html>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Use the Liveblocks hooks and components</StepTitle>
    <StepContent>

      Now that we’ve set up the provider, we can start using the Liveblocks hooks and components.
      We’ll add [`useInboxNotifications`](/docs/api-reference/liveblocks-react#useInboxNotifications)
      to get the current project’s notifications, then we’ll
      use [`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification) and [`InboxNotificationList`](/docs/api-reference/liveblocks-react-ui#InboxNotificationList) to render them.

      ```tsx file="app/page.tsx" highlight="10,13-20"
      "use client";

      import { useInboxNotifications } from "@liveblocks/react/suspense";
      import {
        InboxNotification,
        InboxNotificationList,
      } from "@liveblocks/react-ui";

      export default function Page() {
        const { inboxNotifications } = useInboxNotifications();

        return (
          <InboxNotificationList>
            {inboxNotifications.map((inboxNotification) => (
              <InboxNotification
                key={inboxNotification.id}
                inboxNotification={inboxNotification}
              />
            ))}
          </InboxNotificationList>
        );
      }
      ```

    </StepContent>

  </Step>
   <Step>
    <StepTitle>Import default styles</StepTitle>
    <StepContent>

    The default components come with default styles, you can import them into the
    root layout of your app or directly into a CSS file with `@import`.

    ```tsx file="app/layout.tsx"
    import "@liveblocks/react-ui/styles.css";
    ```

    </StepContent>

  </Step>

   <Step>
    <StepTitle>Trigger a custom notification</StepTitle>
    <StepContent>

      Trigger a custom notification using
      [`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger)
      from a server action or route handler. In this example, a custom `$fileUploaded` notification is sent.

      ```ts
      "use server";

      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      // Call this in your app to create a custom notification
      export async function triggerCustomNotification() {
        // +++
        await liveblocks.triggerInboxNotification({
          // The user that will receive the notification
          userId: "steven@example.com",
          kind: "$fileUploaded",
          subjectId: "my-file",

          // Custom data for this notification
          activityData: {
            file: "https://example.com/my-file.zip",
            status: "pending",
          },
        });
        // +++
      }
      ```
    </StepContent>

  </Step>

  <Step>

  <StepTitle>Render the custom notification</StepTitle>
  <StepContent>

    After triggering the custom notification, modify `InboxNotification` to
    [render it with custom UI](/docs/api-reference/liveblocks-react-ui#Rendering-notification-kinds-differently).

    ```tsx file="app/page.tsx"
    "use client";

    import { useInboxNotifications } from "@liveblocks/react/suspense";
    import {
      InboxNotification,
      InboxNotificationList,
    } from "@liveblocks/react-ui";

    export default function Page() {
      const { inboxNotifications } = useInboxNotifications();

      return (
        <InboxNotificationList>
          {inboxNotifications.map((inboxNotification) => (
            <InboxNotification
              key={inboxNotification.id}
              inboxNotification={inboxNotification}
              // +++
              kinds={{
                $fileUploaded: (props) => (
                  <InboxNotification.Custom
                    {...props}
                    title="File uploaded!"
                    aside={<InboxNotification.Icon>📎</InboxNotification.Icon>}
                  >
                    A new file has been uploaded:
                    <b>{props.inboxNotification.activities[0].file}</b>
                  </InboxNotification.Custom>
                ),
              }}
              // +++
            />
          ))}
        </InboxNotificationList>
      );
    }
    ```

  </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: authenticate and add your users</StepTitle>
    <StepContent>

      Notifications is set up and working now, but each user is anonymous—the next step is to
      authenticate each user as they connect, and attach their name and avatar to their notifications.

      <Button asChild  className="not-markdown">
        <a href="/docs/guides/how-to-add-users-to-liveblocks-notifications">
          Add your users to Notifications
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You’ve set up the foundation to start building a notifications
experience for your React application.

- [API reference](/docs/api-reference/liveblocks-react#Notifications)
- [Component reference](/docs/api-reference/liveblocks-react-ui#Notifications)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "comments-notifications/nextjs-comments-notifications",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with sending custom notifications to Microsoft Teams using
    Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to send custom notifications to Microsoft Teams using using
    Liveblocks Teams using Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to send
[custom notifications](/docs/ready-made-features/notifications/concepts#Custom-notifications)
to Microsoft Teams using methods from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks Node</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>

   <Step>
    <StepTitle>Trigger a custom notification</StepTitle>
    <StepContent>

      Trigger a custom notification using
      [`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger)
      from a server action or route handler. In this example, a custom `$fileUploaded` notification is sent.

      ```ts
      "use server";

      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      // Call this in your app to create a custom notification
      export async function triggerCustomNotification() {
        // +++
        await liveblocks.triggerInboxNotification({
          // The user that will receive the notification
          userId: "steven@example.com",
          kind: "$fileUploaded",
          subjectId: "my-file",

          // Custom data for this notification
          activityData: {
            file: "https://example.com/my-file.zip",
            status: "pending",
          },
        });
        // +++
      }
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Follow our guide</StepTitle>
    <StepContent>

      Follow our step-by-step guide to learn set up MicrosoftTeams notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-notify-users-about-unread-custom-notifications-outside-of-your-app">
          How to notify users with MicrosoftTeams
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You've set up Microsoft Teams notifications in your Next.js
application.

- [Webhooks API reference](/docs/platform/webhooks)
- [Node.js API reference](/docs/api-reference/liveblocks-node)
- [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "https://liveblocks.io/examples/comments-emails",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with sending custom notifications to Slack using Liveblocks and
    Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to send custom notifications to Slack using using Liveblocks
    Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to send
[custom notifications](/docs/ready-made-features/notifications/concepts#Custom-notifications)
to Slack using methods from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks Node</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>

   <Step>
    <StepTitle>Trigger a custom notification</StepTitle>
    <StepContent>

      Trigger a custom notification using
      [`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger)
      from a server action or route handler. In this example, a custom `$fileUploaded` notification is sent.

      ```ts
      "use server";

      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      // Call this in your app to create a custom notification
      export async function triggerCustomNotification() {
        // +++
        await liveblocks.triggerInboxNotification({
          // The user that will receive the notification
          userId: "steven@example.com",
          kind: "$fileUploaded",
          subjectId: "my-file",

          // Custom data for this notification
          activityData: {
            file: "https://example.com/my-file.zip",
            status: "pending",
          },
        });
        // +++
      }
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Follow our guide</StepTitle>
    <StepContent>

      Follow our step-by-step guide to learn set up Slack notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-notify-users-about-unread-custom-notifications-outside-of-your-app">
          How to notify users with Slack
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You've set up Slack notifications in your Next.js application.

- [Webhooks API reference](/docs/platform/webhooks)
- [Node.js API reference](/docs/api-reference/liveblocks-node)
- [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "https://liveblocks.io/examples/comments-emails",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with sending custom notifications with Web Push using
    Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to send custom notifications with Web Push using using Liveblocks
    and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to send
[custom notifications](/docs/ready-made-features/notifications/concepts#Custom-notifications)
with Web Push using methods from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks Node</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Trigger a custom notification</StepTitle>
    <StepContent>

      Trigger a custom notification using
      [`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger)
      from a server action or route handler. In this example, a custom `$fileUploaded` notification is sent.

      ```ts
      "use server";

      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      // Call this in your app to create a custom notification
      export async function triggerCustomNotification() {
        // +++
        await liveblocks.triggerInboxNotification({
          // The user that will receive the notification
          userId: "steven@example.com",
          kind: "$fileUploaded",
          subjectId: "my-file",

          // Custom data for this notification
          activityData: {
            file: "https://example.com/my-file.zip",
            status: "pending",
          },
        });
        // +++
      }
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Follow our guide</StepTitle>
    <StepContent>

      Follow our step-by-step guide to learn set up Web Push notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-notify-users-about-unread-custom-notifications-outside-of-your-app">
          How to notify users with Web Push
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You've set up Web Push notifications in your Next.js
application.

- [Webhooks API reference](/docs/platform/webhooks)
- [Node.js API reference](/docs/api-reference/liveblocks-node)
- [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "https://liveblocks.io/examples/comments-emails",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with receiving custom notifications webhook events using
    Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to receive custom notifications webhook events using using
    Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to receive webhooks events for
[custom notifications](/docs/ready-made-features/notifications/concepts#Custom-notifications)
using methods from [`@liveblocks/node`](/docs/api-reference/liveblocks-node).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks Node</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>

   <Step>
    <StepTitle>Trigger a custom notification</StepTitle>
    <StepContent>

      Trigger a custom notification using
      [`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger)
      from a server action or route handler. In this example, a custom `$fileUploaded` notification is sent.

      ```ts
      "use server";

      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      // Call this in your app to create a custom notification
      export async function triggerCustomNotification() {
        // +++
        await liveblocks.triggerInboxNotification({
          // The user that will receive the notification
          userId: "steven@example.com",
          kind: "$fileUploaded",
          subjectId: "my-file",

          // Custom data for this notification
          activityData: {
            file: "https://example.com/my-file.zip",
            status: "pending",
          },
        });
        // +++
      }
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Follow our guide</StepTitle>
    <StepContent>

      Follow our step-by-step guide to learn set up webhooks notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-notify-users-about-unread-custom-notifications-outside-of-your-app">
          How to notify users with webhooks
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You've set up webhooks notifications in your Next.js
application.

- [Webhooks API reference](/docs/platform/webhooks)
- [Node.js API reference](/docs/api-reference/liveblocks-node)
- [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "https://liveblocks.io/examples/comments-emails",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with emailing collaborative notifications using Liveblocks and
    Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to email collaborative notifications using Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to email
[collaboration notifications](/docs/ready-made-features/notifications/concepts#Collaboration-notifications)
using methods from [`@liveblocks/node`](/docs/api-reference/liveblocks-node).

## Quickstart

<Steps>

  <Step>
      <StepTitle>Have your Liveblocks app ready</StepTitle>
      <StepContent>

        To receive collaboration webhook events, they need to be triggered
        from your front-end application, for example when a new comment is
        added to a thread in your Liveblocks app. If you haven't set up an
        app yet, [get started with Comments](/docs/get-started/comments)
        before continuing.

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Install Liveblocks Node</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Follow our guide</StepTitle>
    <StepContent>

      Follow our step-by-step guide to learn set up email notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-send-email-notifications-of-unread-comments">
          How to send email notifications of unread comments
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You've set up email notifications in your Next.js application.

- [Webhooks API reference](/docs/platform/webhooks)
- [Node.js API reference](/docs/api-reference/liveblocks-node)
- [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "https://liveblocks.io/examples/comments-emails",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with an in-app notification inbox using Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to add an in-app notification inbox using Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to start adding notifications to
your Next.js `/app` directory application using the hooks from
[`@liveblocks/react`](/docs/api-reference/liveblocks-react) and the components
from [`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Create a Liveblocks provider</StepTitle>
    <StepContent>

      Liveblocks Notifications uses the concept of projects, which relate to
      projects in [your dashboard](/dashboard). Notifications are sent between
      users in the same project. To connect and receive notifications, you must
      add [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
      to a client component in your app.

      ```tsx file="app/Providers.tsx" highlight="8-10"
      "use client";

      import { ReactNode } from "react";
      import { LiveblocksProvider } from "@liveblocks/react";

      export function Providers({ children }: { children: ReactNode }) {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            {children}
          </LiveblocksProvider>
        );
      }
    ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add the provider to your layout</StepTitle>
    <StepContent>

      After creating your provider file, it’s time to use it. Import
      your room into your `layout.tsx` file.

      ```tsx file="app/layout.tsx"
      import { Providers } from "./Providers";

      export default function Layout({ children }) {
        return (
          <html>
            <body>
              // +++
              <Providers>
                {children}
              </Providers>
              // +++
            </body>
          </html>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Use the Liveblocks hooks and components</StepTitle>
    <StepContent>

      Now that we’ve set up the provider, we can start using the Liveblocks hooks and components.
      We’ll add [`useInboxNotifications`](/docs/api-reference/liveblocks-react#useInboxNotifications)
      to get the current project’s notifications, then we’ll
      use [`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification) and [`InboxNotificationList`](/docs/api-reference/liveblocks-react-ui#InboxNotificationList) to render them.

      ```tsx file="app/page.tsx" highlight="10,13-20"
      "use client";

      import { useInboxNotifications } from "@liveblocks/react/suspense";
      import {
        InboxNotification,
        InboxNotificationList,
      } from "@liveblocks/react-ui";

      export default function Page() {
        const { inboxNotifications } = useInboxNotifications();

        return (
          <InboxNotificationList>
            {inboxNotifications.map((inboxNotification) => (
              <InboxNotification
                key={inboxNotification.id}
                inboxNotification={inboxNotification}
              />
            ))}
          </InboxNotificationList>
        );
      }
      ```

    </StepContent>

  </Step>
   <Step>
    <StepTitle>Import default styles</StepTitle>
    <StepContent>

    The default components come with default styles, you can import them into the
    root layout of your app or directly into a CSS file with `@import`.

    ```tsx file="app/layout.tsx"
    import "@liveblocks/react-ui/styles.css";
    ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: authenticate and add your users</StepTitle>
    <StepContent>

      Notifications is set up and working now, but each user is anonymous—the next step is to
      authenticate each user as they connect, and attach their name and avatar to their notifications.

      <Button asChild  className="not-markdown">
        <a href="/docs/guides/how-to-add-users-to-liveblocks-notifications">
          Add your users to Notifications
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You’ve set up the foundation to start building a notifications
experience for your React application.

- [API reference](/docs/api-reference/liveblocks-react#Notifications)
- [Component reference](/docs/api-reference/liveblocks-react-ui#Notifications)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "comments-notifications/nextjs-comments-notifications",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with sending collaborative notifications to Microsoft Teams
    using Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to send collaborative notifications to Microsoft Teams using
    Microsoft Teams using Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to send
[collaboration notifications](/docs/ready-made-features/notifications/concepts#Collaboration-notifications)
to Microsoft Teams using methods from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node).

## Quickstart

<Steps>

  <Step>
      <StepTitle>Have your Liveblocks app ready</StepTitle>
      <StepContent>

        To receive collaboration webhook events, they need to be triggered
        from your front-end application, for example when a new comment is
        added to a thread in your Liveblocks app. If you haven't set up an
        app yet, [get started with Comments](/docs/get-started/comments)
        before continuing.

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Install Liveblocks Node</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Follow our guide</StepTitle>
    <StepContent>

      Follow our step-by-step guide to learn set up Microsoft Teams notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-notify-users-about-unread-comments-outside-of-your-app">
          How to notify users with Microsoft Teams
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You've set up Microsoft Teams notifications in your Next.js
application.

- [Webhooks API reference](/docs/platform/webhooks)
- [Node.js API reference](/docs/api-reference/liveblocks-node)
- [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "https://liveblocks.io/examples/comments-emails",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with sending collaborative notifications to Slack using
    Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to send collaborative notifications to Slack using using
    Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to send
[collaboration notifications](/docs/ready-made-features/notifications/concepts#Collaboration-notifications)
to Slack using methods from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node).

## Quickstart

<Steps>

  <Step>
      <StepTitle>Have your Liveblocks app ready</StepTitle>
      <StepContent>

        To receive collaboration webhook events, they need to be triggered
        from your front-end application, for example when a new comment is
        added to a thread in your Liveblocks app. If you haven't set up an
        app yet, [get started with Comments](/docs/get-started/comments)
        before continuing.

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Install Liveblocks Node</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Follow our guide</StepTitle>
    <StepContent>

      Follow our step-by-step guide to learn set up Slack notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-notify-users-about-unread-comments-outside-of-your-app">
          How to notify users with Slack
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You've set up Slack notifications in your Next.js application.

- [Webhooks API reference](/docs/platform/webhooks)
- [Node.js API reference](/docs/api-reference/liveblocks-node)
- [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "https://liveblocks.io/examples/comments-emails",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with sending collaborative notifications with Web Push using
    Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to send collaborative notifications with Web Push using
    Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to send
[collaboration notifications](/docs/ready-made-features/notifications/concepts#Collaboration-notifications)
to Web Push using methods from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node).

## Quickstart

<Steps>

  <Step>
      <StepTitle>Have your Liveblocks app ready</StepTitle>
      <StepContent>

        To receive collaboration webhook events, they need to be triggered
        from your front-end application, for example when a new comment is
        added to a thread in your Liveblocks app. If you haven't set up an
        app yet, [get started with Comments](/docs/get-started/comments)
        before continuing.

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Install Liveblocks Node</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Follow our guide</StepTitle>
    <StepContent>

      Follow our step-by-step guide to learn set up Web Push notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-notify-users-about-unread-comments-outside-of-your-app">
          How to notify users with Web Push notifications
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You've set up Web Push notifications in your Next.js
application.

- [Webhooks API reference](/docs/platform/webhooks)
- [Node.js API reference](/docs/api-reference/liveblocks-node)
- [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "https://liveblocks.io/examples/comments-emails",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with receiving collaborative notifications with webhooks using
    Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to receive collaborative notifications with webhooks using
    Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow this guide to receive webhooks events for
[collaboration notifications](/docs/ready-made-features/notifications/concepts#Collaboration-notifications)
using methods from [`@liveblocks/node`](/docs/api-reference/liveblocks-node).

## Quickstart

<Steps>

  <Step>
      <StepTitle>Have your Liveblocks app ready</StepTitle>
      <StepContent>

        To receive collaboration webhook events, they need to be triggered
        from your front-end application, for example when a new comment is
        added to a thread in your Liveblocks app. If you haven't set up an
        app yet, [get started with Comments](/docs/get-started/comments)
        before continuing.

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Install Liveblocks Node</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Follow our guide</StepTitle>
    <StepContent>

      Follow our step-by-step guide to learn set up webhooks for notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-notify-users-about-unread-comments-outside-of-your-app">
          How to notify users with webhooks
        </a>
      </Button>

    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You’ve set up external notifications in your Next.js
application.

- [Webhooks API reference](/docs/platform/webhooks)
- [Node.js API reference](/docs/api-reference/liveblocks-node)
- [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "https://liveblocks.io/examples/comments-emails",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with a Tiptap text editor using Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Tiptap text editor using Liveblocks and Next.js."
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Next.js application using the APIs from the
[`@liveblocks/react-tiptap`](/docs/api-reference/liveblocks-react-tiptap)
package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks and Tiptap</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/react-tiptap @tiptap/react @tiptap/starter-kit
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Create a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. When using Next.js’ `/app` router,
      we recommend creating your room in a `Room.tsx` file in the same directory
      as your current route.

      Set up a Liveblocks client with
      [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider),
      join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider),
      and use [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense)
      to add a loading spinner to your app.

      ```tsx file="app/Room.tsx" highlight="12-18"
      "use client";

      import { ReactNode } from "react";
      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";

      export function Room({ children }: { children: ReactNode }) {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                {children}
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

  </StepContent>

</Step>
<Step>
  <StepTitle>Add the Liveblocks room to your page</StepTitle>
  <StepContent>

    After creating your room file, it’s time to join it. Import
    your room into your `page.tsx` file, and place
    your collaborative app components inside it.

    ```tsx file="app/page.tsx" highlight="6-8"
    import { Room } from "./Room";
    import { Editor } from "./Editor";

    export default function Page() {
      return (
        <Room>
          <Editor/>
        </Room>
      );
    }
    ```

  </StepContent>

</Step>
<Step>
  <StepTitle>Set up the collaborative Tiptap text editor</StepTitle>
  <StepContent>

    Now that we set up Liveblocks, we can start integrating Tiptap and Liveblocks in the `Editor.tsx` file.
    To make the editor collaborative, we can add
    [`useLiveblocksExtension`](/docs/api-reference/liveblocks-react-tiptap#useLiveblocksExtension)
    from `@liveblocks/react-tiptap`. [`FloatingToolbar`](/docs/api-reference/liveblocks-react-tiptap#FloatingToolbar)
    adds a text selection toolbar.

    ```tsx file="app/Editor.tsx"
    "use client";

    import { useLiveblocksExtension, FloatingToolbar } from "@liveblocks/react-tiptap";
    import { useEditor, EditorContent } from "@tiptap/react";
    import StarterKit from "@tiptap/starter-kit";
    import { Threads } from "./Threads";

    export function Editor() {
      const liveblocks = useLiveblocksExtension();

      const editor = useEditor({
        extensions: [
          liveblocks,
          StarterKit.configure({
            // The Liveblocks extension comes with its own history handling
            undoRedo: false,
          }),
        ],
        immediatelyRender: false,
      });

      return (
        <div>
          <EditorContent editor={editor} className="editor" />
          <Threads editor={editor} />
          <FloatingToolbar editor={editor} />
        </div>
      );
    }
    ```

</StepContent>

</Step>
<Step>
  <StepTitle>Render threads and composer</StepTitle>
  <StepContent>

    To add [Comments](/docs/ready-made-features/comments) to your text editor, we need to import a thread composer
    and list each thread on the page. Create a `Threads.tsx` file that uses [`FloatingComposer`](/docs/api-reference/liveblocks-react-tiptap#FloatingComposer)
    for creating new threads, alongside [`AnchoredThreads`](/docs/api-reference/liveblocks-react-tiptap#AnchoredThreads) and [`FloatingThreads`](/docs/api-reference/liveblocks-react-tiptap#FloatingThreads) for displaying threads on desktop and mobile.

    ```tsx file="app/Threads.tsx"
    import { useThreads } from "@liveblocks/react/suspense";
    import {
      AnchoredThreads,
      FloatingComposer,
      FloatingThreads,
    } from "@liveblocks/react-tiptap";
    import { Editor } from "@tiptap/react";

    export function Threads({ editor }: { editor: Editor | null }) {
      const { threads } = useThreads({ query: { resolved: false } });

      return (
        <>
          <div className="anchored-threads">
            <AnchoredThreads editor={editor} threads={threads} />
          </div>
          <FloatingThreads
            editor={editor}
            threads={threads}
            className="floating-threads"
          />
          <FloatingComposer editor={editor} className="floating-composer" />
        </>
      );
    }
    ```

</StepContent>

</Step>
<Step>
  <StepTitle>Style your editor</StepTitle>
  <StepContent>

    Tiptap text editor is unstyled by default, so we can create some custom styles for it in a `globals.css` file. Import `globals.css`, alongside the default Liveblocks styles. You can import them into the
    root layout of your app or directly into a CSS file with `@import`.

    ```css file="app/globals.css" isCollapsed isCollapsable
    .editor {
      position: relative;
      display: flex;
      width: 100%;
      height: 100%;
    }

    .tiptap {
      padding: 2px 12px;
      outline: none;
      width: 100%;
    }

    /* For mobile */
    .floating-threads {
      display: none;
    }

    /* For desktop */
    .anchored-threads {
      display: block;
      max-width: 300px;
      width: 100%;
      position: absolute;
      right: 12px;
    }

    @media (max-width: 640px) {
      .floating-threads {
        display: block;
      }

      .anchored-threads {
        display: none;
      }
    }
    ```

    ```tsx file="app/layout.tsx"
    import "@liveblocks/react-ui/styles.css";
    import "@liveblocks/react-tiptap/styles.css";
    import "./globals.css";
    ```

  </StepContent>

</Step>
<Step>
  <StepTitle>Next: authenticate and add your users</StepTitle>
  <StepContent>
    Text Editor is set up and working now, but each user is anonymous—the next step is to
    authenticate each user as they connect, and attach their name, color, and avatar, to their cursors and mentions.

    <Button asChild  className="not-markdown">
      <a href="/docs/guides/how-to-add-users-to-liveblocks-text-editor">
        Add your users to Text Editor
      </a>
    </Button>

  </StepContent>

</Step>
<Step lastStep>
  <StepTitle>Optional: add more features</StepTitle>
  <StepContent>
   Tiptap is easy to extend, and a number of extensions are already available, making it possible to quickly create complex rich-text applications. For example you can enable features
    such as tables, text highlights, embedded images, and more. This is all supported using Liveblocks.

    <Button asChild  className="not-markdown">
      <a href="https://tiptap.dev/docs/editor/extensions/overview">
        Find more extensions
      </a>
    </Button>

  </StepContent>

</Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Tiptap text editor inside your React application.

- [@liveblocks/react-tiptap API Reference](/docs/api-reference/liveblocks-react-tiptap)
- [Tiptap guides](/docs/guides?technologies=tiptap)
- [Tiptap website](https://tiptap.dev)

---

## Examples using Tiptap

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Text Editor",
      slug: "collaborative-text-editor/nextjs-tiptap",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Advanced Collaborative Text Editor",
      slug: "collaborative-text-editor-advanced/nextjs-tiptap-advanced",
      image: "/images/examples/thumbnails/text-editor-advanced.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with a collaborative Tldraw whiteboard using Liveblocks and
    Next.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to add a collaborative Tldraw whiteboard using Liveblocks and
    Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Next.js and Tldraw app using hooks from
[`@liveblocks/react`](/docs/api-reference/liveblocks-react).

## Quickstart

<Steps>

  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react tldraw
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>

<Step>
    <StepTitle>Add Tldraw types to `liveblocks.config.ts`</StepTitle>
    <StepContent>

    Edit your new `liveblocks.config.ts` file to include the following types for Tldraw.

      ```ts file="liveblocks.config.ts"
      declare global {
        interface Liveblocks {
          Presence: {
            presence: any;
          };

          Storage: {
            records: LiveMap<string, any>;
          };

          UserMeta: {
            id: string;
            info: {
              name: string;
              color: string;
              avatar: string;
            };
          };
        }
      }
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Create a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. When using Next.js’ `/app` router,
      we recommend creating your room in a `Room.tsx` file in the same directory
      as your current route.

      Set up a Liveblocks client with
      [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider),
      join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider),
      and use [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense)
      to add a loading spinner to your app.

      ```tsx file="app/Room.tsx" highlight="12-18"
      "use client";

      import { ReactNode } from "react";
      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";

      export function Room({ children }: { children: ReactNode }) {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                {children}
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add the Liveblocks room to your page</StepTitle>
    <StepContent>

      After creating your room file, it’s time to join it. Import
      your room into your `page.tsx` file, and place
      your collaborative app components inside it.

      ```tsx file="app/page.tsx" highlight="6-8"
      import { Room } from "./Room";
      import { TldrawApp } from "./TldrawApp";

      export default function Page() {
        return (
          <Room>
            <TldrawApp />
          </Room>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add the Tldraw component</StepTitle>
    <StepContent>

      Now that we’re connected to a room, add the `Tldraw` component, importing a custom `useStorageStore` hook.

      ```tsx file="app/TldrawApp.tsx"
      "use client";

      import { useStorageStore } from "./useStorageStore";

      function StorageTldraw() {
        const id = useSelf((me) => me.id);
        const info = useSelf((me) => me.info);

        const store = useStorageStore({
          user: { id, color: info?.color || "red", name: info?.name || "Guest" },
        });

        return (
          <div style={{ height: "100%", width: "100vw" }}>
            <Tldraw store={store} autoFocus />
          </div>
        );
      }
      ```

    </StepContent>

  </Step>

<Step>
    <StepTitle>Synchronize Tldraw with Liveblocks</StepTitle>
    <StepContent>

      Create the `useStorageStore` hook to synchronize Tldraw state with
      Liveblocks Storage, and your whiteboard should be synchronized.

      ```tsx file="app/useStorageStore.ts" isCollapsable isCollapsed
      import { useEffect, useState } from "react";
      import { useRoom } from "@liveblocks/react/suspense";
      import {
        computed,
        createPresenceStateDerivation,
        createTLStore,
        react,
        defaultShapeUtils,
        DocumentRecordType,
        InstancePresenceRecordType,
        PageRecordType,
        IndexKey,
        TLAnyShapeUtilConstructor,
        TLDocument,
        TLInstancePresence,
        TLPageId,
        TLRecord,
        TLStoreEventInfo,
        TLStoreWithStatus,
      } from "tldraw";

      export function useStorageStore({
        shapeUtils = [],
        user,
      }: Partial<{
        hostUrl: string;
        version: number;
        shapeUtils: TLAnyShapeUtilConstructor[];
        user: {
          id: string;
          color: string;
          name: string;
        };
      }>) {
        // Get Liveblocks room
        const room = useRoom();

        // Set up tldraw store and status
        const [store] = useState(() => {
          const store = createTLStore({
            shapeUtils: [...defaultShapeUtils, ...shapeUtils],
          });
          return store;
        });

        const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
          status: "loading",
        });

        useEffect(() => {
          const unsubs: (() => void)[] = [];
          // State is already initialized to "loading", no need to set it again

          async function setup() {
            // Get Liveblocks Storage values
            const { root } = await room.getStorage();
            const liveRecords = root.get("records");

            // Initialize tldraw with records from Storage
            store.clear();
            store.put(
              [
                DocumentRecordType.create({
                  id: "document:document" as TLDocument["id"],
                }),
                PageRecordType.create({
                  id: "page:page" as TLPageId,
                  name: "Page 1",
                  index: "a1" as IndexKey,
                }),
                ...[...liveRecords.values()],
              ],
              "initialize"
            );

            // Sync tldraw changes with Storage
            unsubs.push(
              store.listen(
                ({ changes }: TLStoreEventInfo) => {
                  room.batch(() => {
                    Object.values(changes.added).forEach((record) => {
                      liveRecords.set(record.id, record);
                    });

                    Object.values(changes.updated).forEach(([_, record]) => {
                      liveRecords.set(record.id, record);
                    });

                    Object.values(changes.removed).forEach((record) => {
                      liveRecords.delete(record.id);
                    });
                  });
                },
                { source: "user", scope: "document" }
              )
            );

            // Sync tldraw changes with Presence
            function syncStoreWithPresence({ changes }: TLStoreEventInfo) {
              room.batch(() => {
                Object.values(changes.added).forEach((record) => {
                  room.updatePresence({ [record.id]: record });
                });

                Object.values(changes.updated).forEach(([_, record]) => {
                  room.updatePresence({ [record.id]: record });
                });

                Object.values(changes.removed).forEach((record) => {
                  room.updatePresence({ [record.id]: null });
                });
              });
            }

            unsubs.push(
              store.listen(syncStoreWithPresence, {
                source: "user",
                scope: "session",
              })
            );

            unsubs.push(
              store.listen(syncStoreWithPresence, {
                source: "user",
                scope: "presence",
              })
            );

            // Update tldraw when Storage changes
            unsubs.push(
              room.subscribe(
                liveRecords,
                (storageChanges) => {
                  const toRemove: TLRecord["id"][] = [];
                  const toPut: TLRecord[] = [];

                  for (const update of storageChanges) {
                    if (update.type !== "LiveMap") {
                      return;
                    }

                    for (const [id, { type }] of Object.entries(update.updates)) {
                      switch (type) {
                        // Object deleted from Liveblocks, remove from tldraw
                        case "delete": {
                          toRemove.push(id as TLRecord["id"]);
                          break;
                        }

                        // Object updated on Liveblocks, update tldraw
                        case "update": {
                          const curr = update.node.get(id);
                          if (curr) {
                            toPut.push(curr as any as TLRecord);
                          }
                          break;
                        }
                      }
                    }
                  }

                  // Update tldraw with changes
                  store.mergeRemoteChanges(() => {
                    if (toRemove.length) {
                      store.remove(toRemove);
                    }
                    if (toPut.length) {
                      store.put(toPut);
                    }
                  });
                },
                { isDeep: true }
              )
            );

            // Set user's info
            const userPreferences = computed<{
              id: string;
              color: string;
              name: string;
            }>("userPreferences", () => {
              if (!user) {
                throw new Error("Failed to get user");
              }
              return {
                id: user.id,
                color: user.color,
                name: user.name,
              };
            });

            // Unique ID for this session is their connectionId
            const connectionIdString = "" + (room.getSelf()?.connectionId || 0);

            // Set both
            const presenceDerivation = createPresenceStateDerivation(
              userPreferences,
              InstancePresenceRecordType.createId(connectionIdString)
            )(store);

            // Update presence with tldraw values
            room.updatePresence({
              presence: presenceDerivation.get() ?? null,
            });

            // Update Liveblocks when tldraw presence changes
            unsubs.push(
              react("when presence changes", () => {
                const presence = presenceDerivation.get() ?? null;
                requestAnimationFrame(() => {
                  room.updatePresence({ presence });
                });
              })
            );

            // Sync Liveblocks presence with tldraw
            unsubs.push(
              room.subscribe("others", (others, event) => {
                const toRemove: TLInstancePresence["id"][] = [];
                const toPut: TLInstancePresence[] = [];

                switch (event.type) {
                  // A user disconnected from Liveblocks
                  case "leave": {
                    if (event.user.connectionId) {
                      toRemove.push(
                        InstancePresenceRecordType.createId(
                          `${event.user.connectionId}`
                        )
                      );
                    }
                    break;
                  }

                  // Others was reset, e.g. after losing connection and returning
                  case "reset": {
                    others.forEach((other) => {
                      toRemove.push(
                        InstancePresenceRecordType.createId(`${other.connectionId}`)
                      );
                    });
                    break;
                  }

                  // A user entered or their presence updated
                  case "enter":
                  case "update": {
                    const presence = event?.user?.presence;
                    if (presence?.presence) {
                      toPut.push(event.user.presence.presence);
                    }
                  }
                }

                // Update tldraw with changes
                store.mergeRemoteChanges(() => {
                  if (toRemove.length) {
                    store.remove(toRemove);
                  }
                  if (toPut.length) {
                    store.put(toPut);
                  }
                });
              })
            );

            setStoreWithStatus({
              store,
              status: "synced-remote",
              connectionStatus: "online",
            });
          }

          setup();

          return () => {
            unsubs.forEach((fn) => fn());
            unsubs.length = 0;
          };
        }, [room, store, user]);

        return storeWithStatus;
      }
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>

      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation to start building
collaborative experiences for your React application.

- [@liveblocks/react API Reference](/docs/api-reference/liveblocks-react)
- [Next.js and React guides](/docs/guides?technologies=nextjs%2Creact)
- [How to use Liveblocks Presence with React](/docs/guides/how-to-use-liveblocks-presence-with-react)
- [How to use Liveblocks Storage with React](/docs/guides/how-to-use-liveblocks-storage-with-react)

---

## Examples using Next.js

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative To-do List",
      slug: "collaborative-todo-list/nextjs-todo-list",
      image: "/images/examples/thumbnails/collaborative-todo-list.jpg",
    }}
    technologies={["nextjs", "vuejs", "sveltekit", "solidjs", "javascript"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Live Avatar Stack",
      slug: "live-avatar-stack/nextjs-live-avatars",
      image: "/images/examples/thumbnails/live-avatar-stack.jpg",
    }}
    technologies={["nextjs", "nuxtjs", "vuejs", "sveltekit", "solidjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Spreadsheet",
      slug: "collaborative-spreadsheet-advanced/nextjs-spreadsheet-advanced",
      image:
        "/images/examples/thumbnails/collaborative-spreadsheet-advanced.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Whiteboard",
      slug: "collaborative-whiteboard-advanced/nextjs-whiteboard-advanced",
      image:
        "/images/examples/thumbnails/collaborative-whiteboard-advanced.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with Liveblocks and Next.js"
  parentTitle: "Quickstart"
  description: "Learn how to get started with Liveblocks and Next.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Next.js `/app` directory application using the hooks from
the [`@liveblocks/react`](/docs/api-reference/liveblocks-react) package.

## Quickstart

<Steps>

  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Create a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. When using Next.js’ `/app` router,
      we recommend creating your room in a `Room.tsx` file in the same directory
      as your current route.

      Set up a Liveblocks client with
      [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider),
      join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider),
      and use [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense)
      to add a loading spinner to your app.

      ```tsx file="app/Room.tsx" highlight="12-18"
      "use client";

      import { ReactNode } from "react";
      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";

      export function Room({ children }: { children: ReactNode }) {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                {children}
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add the Liveblocks room to your page</StepTitle>
    <StepContent>

      After creating your room file, it’s time to join it. Import
      your room into your `page.tsx` file, and place
      your collaborative app components inside it.

      ```tsx file="app/page.tsx" highlight="6-8"
      import { Room } from "./Room";
      import { CollaborativeApp } from "./CollaborativeApp";

      export default function Page() {
        return (
          <Room>
            <CollaborativeApp />
          </Room>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Use the Liveblocks hooks</StepTitle>
    <StepContent>

      Now that we’re connected to a room, we can start using the Liveblocks hooks.
      The first we’ll add is [`useOthers`](/docs/api-reference/liveblocks-react#useOthers), a hook that provides information
      about which other users are connected to the room.

      ```tsx file="app/CollaborativeApp.tsx" highlight="6"
      "use client";

      import { useOthers } from "@liveblocks/react/suspense";

      export function CollaborativeApp() {
        const others = useOthers();
        const userCount = others.length;
        return <div>There are {userCount} other user(s) online</div>;
      }
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>

      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation to start building
collaborative experiences for your React application.

- [@liveblocks/react API Reference](/docs/api-reference/liveblocks-react)
- [Next.js and React guides](/docs/guides?technologies=nextjs%2Creact)
- [How to use Liveblocks Presence with React](/docs/guides/how-to-use-liveblocks-presence-with-react)
- [How to use Liveblocks Storage with React](/docs/guides/how-to-use-liveblocks-storage-with-react)

---

## Examples using Next.js

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative To-do List",
      slug: "collaborative-todo-list/nextjs-todo-list",
      image: "/images/examples/thumbnails/collaborative-todo-list.jpg",
    }}
    technologies={["nextjs", "vuejs", "sveltekit", "solidjs", "javascript"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Live Avatar Stack",
      slug: "live-avatar-stack/nextjs-live-avatars",
      image: "/images/examples/thumbnails/live-avatar-stack.jpg",
    }}
    technologies={["nextjs", "nuxtjs", "vuejs", "sveltekit", "solidjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Spreadsheet",
      slug: "collaborative-spreadsheet-advanced/nextjs-spreadsheet-advanced",
      image:
        "/images/examples/thumbnails/collaborative-spreadsheet-advanced.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Whiteboard",
      slug: "collaborative-whiteboard-advanced/nextjs-whiteboard-advanced",
      image:
        "/images/examples/thumbnails/collaborative-whiteboard-advanced.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with an AI chat using Liveblocks and React"
  parentTitle: "Quickstart"
  description:
    "Learn how to get started with an AI chat using Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding an
advanced AI chat to your React application using the hooks from
[`@liveblocks/react`](/docs/api-reference/liveblocks-react) and the components
from [`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/node
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up an authentication route</StepTitle>
    <StepContent>

      Create an [authentication](/docs/authentication) endpoint at `/api/liveblocks-auth`,
      passing a unique user ID to [`prepareSession`](/docs/api-reference/liveblocks-node#prepareSession).
      Each user has their own set of private chats, and this allows Liveblocks to identify who is connecting.
      Add your secret key from the [dashboard](/dashboard).

      ```tsx file="Your endpoint at /api/liveblocks-auth"
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "{{SECRET_KEY}}",
      });

      export async function POST() {
        // Get the current user from your database
        const user = {
          // +++
          id: "user-1",
          // +++
          info: {
            // Optional. `name`, `avatar`, `color`, custom info.
          },
        };

        // Create a session for the current user
        // +++
        const session = liveblocks.prepareSession(user.id, {
          userInfo: user.info,
        });
        // +++

        // Give the user access to AI chats
        session.allow(`*`, session.FULL_ACCESS);

        // Authorize the user and return the result
        const { status, body } = await session.authorize();
        return new Response(body, { status });
      }
      ```
    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      To connect the user to their chats, you must
      add [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
      to a client component in your app.

      ```tsx file="App.tsx"
      "use client";

      import { LiveblocksProvider } from "@liveblocks/react/suspense";
      import { MyApp } from "./MyApp";

      export default function App() {
        return (
          // +++
          <LiveblocksProvider authEndpoint="/api/liveblocks-auth">
            <MyApp />
          </LiveblocksProvider>
          // +++
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Add the AI chat component</StepTitle>
    <StepContent>

      Now you can add the [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat)
      component to your application. This component renders a fully-featured AI
      chat interface with message history and a composer. You can add
      [tools](/docs/ready-made-features/ai-copilots/tools), [knowledge](/docs/ready-made-features/ai-copilots/knowledge),
      and use [hooks](/docs/ready-made-features/ai-copilots/hooks).

      ```tsx file="MyApp.tsx"
      "use client";

      import { AiChat } from "@liveblocks/react-ui";
      import { RegisterAiTool, RegisterAiKnowledge } from "@liveblocks/react";

      export function MyApp() {
        return (
          <div style={{ height: 500, width: 500 }}>
            // +++
            <AiChat
              chatId="my-first-chat"
              // copilotId="co_aH1j2..."
            />
            // +++
            {/* <RegisterAiTool ... /> and <RegisterAiKnowledge ... /> */}
          </div>
        );
      }
      ```

    </StepContent>

  </Step>
   <Step>
    <StepTitle>Import default styles</StepTitle>
    <StepContent>

    The default components come with default styles, you can import them into the
    root of your app or directly into a CSS file with `@import`.

    ```tsx
    import "@liveblocks/react-ui/styles.css";
    ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: create a copilot</StepTitle>
    <StepContent>

      AI Copilots is set up and working, but it’s using our default AI model
      and prompt—the next step is to create a custom copilot
      using your preferred AI provider and configuration.

      <Button asChild  className="not-markdown">
        <a href="/docs/ready-made-features/ai-copilots/copilots">
          Create a copilot
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You’ve set up the foundation to start building an AI copilot
experience for your React application.

- [Overview of all features and where to start](/docs/ready-made-features/ai-copilots)
- [@liveblocks/react API reference](/docs/api-reference/liveblocks-react#AI-Copilots)
- [@liveblocks/react-ui API reference](/docs/api-reference/liveblocks-react-ui#AI-Copilots)
- [How to use a fallback AI model](/docs/guides/how-to-use-fallback-ai-models-in-ai-copilots)

---

## Examples using React

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "AI Chats",
      slug: "ai-chats/nextjs-ai-chats",
      image: "/images/examples/thumbnails/ai-chats.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "AI Reports Dashboard",
      slug: "ai-dashboard-reports/nextjs-ai-dashboard-reports",
      image: "/images/examples/thumbnails/ai-reports-dashboard.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "AI Support Chat",
      slug: "ai-support-chat/nextjs-ai-support-chat",
      image: "/images/examples/thumbnails/ai-support.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "AI App Builder",
      slug: "ai-app-builder/nextjs-ai-app-builder",
      image: "/images/examples/thumbnails/ai-app-builder.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started a BlockNote text editor using Liveblocks and React"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a BlockNote text editor using Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your React application using the APIs from the
[`@liveblocks/react-blocknote`](/docs/api-reference/liveblocks-react-blocknote)
package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks and BlockNote</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/react-blocknote @blocknote/core @blocknote/react @blocknote/mantine
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. Set up a Liveblocks client with [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), and join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider).

      ```tsx file="App.tsx" highlight="11-15"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              {/* ... */}
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      After setting up the room, you can add collaborative components inside it, using
      [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense) to add loading spinners to your app.

      ```tsx file="App.tsx" highlight="14-16"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                <Editor />
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the collaborative BlockNote text editor</StepTitle>
    <StepContent>

      Now that we set up Liveblocks, we can start integrating BlockNote and Liveblocks in the `Editor.tsx` file.
    To make the editor collaborative, we can add
    [`useCreateBlockNoteWithLiveblocks`](/docs/api-reference/liveblocks-react-blocknote#useCreateBlockNoteWithLiveblocks)
    from `@liveblocks/react-blocknote`.

    ```tsx file="Editor.tsx"
    "use client";

    import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";
    import { BlockNoteView } from "@blocknote/mantine";
    import { Threads } from "./Threads";

    export function Editor() {
      const editor = useCreateBlockNoteWithLiveblocks({});

      return (
        <div>
          <BlockNoteView editor={editor} className="editor" />
          <Threads editor={editor} />
        </div>
      );
    }
    ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Render threads and composer</StepTitle>
    <StepContent>

     To add [Comments](/docs/ready-made-features/comments) to your text editor, we need to import a thread composer
    and list each thread on the page. Create a `Threads.tsx` file that uses [`FloatingComposer`](/docs/api-reference/liveblocks-react-blocknote#FloatingComposer)
    for creating new threads, alongside [`AnchoredThreads`](/docs/api-reference/liveblocks-react-blocknote#AnchoredThreads) and [`FloatingThreads`](/docs/api-reference/liveblocks-react-blocknote#FloatingThreads) for displaying threads on desktop and mobile.

    ```tsx file="Threads.tsx"
    import { useThreads } from "@liveblocks/react/suspense";
    import {
      AnchoredThreads,
      FloatingComposer,
      FloatingThreads,
    } from "@liveblocks/react-blocknote";
    import { BlockNoteEditor } from "@blocknote/core";

    export function Threads({ editor }: { editor: BlockNoteEditor | null }) {
      const { threads } = useThreads({ query: { resolved: false } });

      if (!editor) {
        return null;
      }

      return (
        <>
          <div className="anchored-threads">
            <AnchoredThreads editor={editor} threads={threads} />
          </div>
          <FloatingThreads
            editor={editor}
            threads={threads}
            className="floating-threads"
          />
          <FloatingComposer editor={editor} className="floating-composer" />
        </>
      );
    }
    ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Style your editor</StepTitle>
    <StepContent>

      We can create some custom styles to position the editor and threads—import `globals.css`, alongside the default Liveblocks styles. You can import them into the
    root layout of your app or directly into a CSS file with `@import`.

    ```css file="app/globals.css" isCollapsed isCollapsable
    html {
      font-family: Inter, sans-serif;
      background: #f9f9f9;
    }

    @media (prefers-color-scheme: dark) {
      html {
        background: #0c0c0c;
      }
    }

    .editor {
      position: absolute;
      inset: 0;
      max-width: 1024px;
      margin: 0 auto;
      padding: 48px 0;
    }

    .bn-editor {
      padding: 36px 52px;
      min-height: 100%;
    }

    /* For mobile */
    .floating-threads {
      display: none;
    }

    /* For desktop */
    .anchored-threads {
      display: block;
      max-width: 300px;
      width: 100%;
      position: absolute;
      right: 12px;
    }

    @media (max-width: 640px) {
      .floating-threads {
        display: block;
      }

      .anchored-threads {
        display: none;
      }
    }
    ```

    ```tsx file="app/layout.tsx"
    import "@blocknote/core/fonts/inter.css";
    import "@blocknote/mantine/style.css";
    import "@liveblocks/react-ui/styles.css";
    import "@liveblocks/react-ui/styles/dark/media-query.css";
    import "@liveblocks/react-tiptap/styles.css";
    import "./globals.css";
    ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Next: authenticate and add your users</StepTitle>
    <StepContent>
      Text Editor is set up and working now, but each user is anonymous—the next step is to
      authenticate each user as they connect, and attach their name, color, and avatar, to their cursors and mentions.

      <Button asChild  className="not-markdown">
        <a href="/docs/guides/how-to-add-users-to-liveblocks-text-editor">
          Add your users to Text Editor
        </a>
      </Button>
    </StepContent>

  </Step>
   <Step lastStep>
    <StepTitle>Optional: add more features</StepTitle>
    <StepContent>
    BlockNote is easy to extend, and a number of extensions are already available, making it possible to quickly create complex rich-text applications. Any additional features you add will be supported using Liveblocks.

    <Button asChild  className="not-markdown">
      <a href="https://www.blocknotejs.org/docs">
        Read the BlockNote docs
      </a>
    </Button>

  </StepContent>

  </Step>

</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
BlockNote text editor inside your React application.

- [@liveblocks/react-blocknote API Reference](/docs/api-reference/liveblocks-react-blocknote)
- [BlockNote guides](/docs/guides?technologies=blocknote)
- [BlockNote website](https://www.blocknotejs.org/)

---

## Examples using BlockNote

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Text Editor",
      slug: "collaborative-text-editor/nextjs-blocknote",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with commenting using Liveblocks and React"
  parentTitle: "Quickstart"
  description:
    "Learn how to add a commenting experience to your application using
    Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding a
commenting experience to your React application using the hooks from
[`@liveblocks/react`](/docs/api-reference/liveblocks-react) and the components
from [`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. Set up a Liveblocks client with [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), and join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider).

      ```tsx file="App.tsx" highlight="11-15"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
      } from "@liveblocks/react/suspense";
      import { Room } from "./Room";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              {/* ... */}
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      After setting up the room, you can add collaborative components inside it, using
      [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense) to add loading spinners to your app.

      ```tsx file="App.tsx" highlight="14-16"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";
      import { Room } from "./Room";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                <Room />
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Use the Liveblocks hooks and components</StepTitle>
    <StepContent>

      Now that we’re connected to a room, we can start using the Liveblocks hooks and components.
      We’ll add [`useThreads`](/docs/api-reference/liveblocks-react#useThreads) to get the threads in the room, then we’ll use the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component
      to render them. Finally, we’ll add a way to create threads by adding a [`Composer`](/docs/api-reference/liveblocks-react-ui#Composer).

      ```tsx file="Room.tsx" highlight="7,11-14"
      "use client";

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

      export function Room() {
        const { threads } = useThreads();

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

    </StepContent>

  </Step>
   <Step>
    <StepTitle>Import default styles</StepTitle>
    <StepContent>

    The default components come with default styles, you can import them into the
    root of your app or directly into a CSS file with `@import`.

    ```tsx
    import "@liveblocks/react-ui/styles.css";
    ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: authenticate and add your users</StepTitle>
    <StepContent>

      Comments is set up and working now, but each user is anonymous—the next step is to
      authenticate each user as they connect, and attach their name and avatar to their comments.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-add-users-to-liveblocks-comments">
          Add your users to Comments
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You’ve set up the foundation to start building a commenting
experience for your React application.

- [API Reference](/docs/api-reference/liveblocks-react-ui)
- [How to send email notifications when comments are created](/docs/guides/how-to-send-email-notifications-when-comments-are-created)

---

## Examples using React

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments",
      slug: "comments/react-comments",
      image: "/images/examples/thumbnails/comments.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Video Comments",
      slug: "video-comments/nextjs-comments-video",
      image: "/images/examples/thumbnails/comments-video.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Overlay Comments",
      slug: "overlay-comments/nextjs-comments-overlay",
      image: "/images/examples/thumbnails/comments-overlay.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Text Editor Comments",
      slug: "text-editor-comments/nextjs-comments-tiptap",
      image: "/images/examples/thumbnails/comments-text-editor.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with a Lexical text editor using Liveblocks and React"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Lexical text editor using Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your React application using the APIs from the
[`@liveblocks/react-lexical`](/docs/api-reference/liveblocks-react-lexical)
package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks and Lexical</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/react-lexical lexical @lexical/react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. Set up a Liveblocks client with [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), and join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider).

      ```tsx file="App.tsx" highlight="11-15"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              {/* ... */}
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      After setting up the room, you can add collaborative components inside it, using
      [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense) to add loading spinners to your app.

      ```tsx file="App.tsx" highlight="14-16"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                <Editor />
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the collaborative Lexical text editor</StepTitle>
    <StepContent>

      Now that we set up Liveblocks, we can start integrating Lexical and Liveblocks in the `Editor.tsx` file.
      To make the editor collaborative, we can use
      [`LiveblocksPlugin`](/docs/api-reference/liveblocks-react-lexical#LiveblocksPlugin)
      from `@liveblocks/react-lexical`. [`FloatingToolbar`](/docs/api-reference/liveblocks-react-lexical#FloatingToolbar)
      adds a text selection toolbar.


      ```tsx file="Editor.tsx"
      "use client";

      import { LexicalComposer } from "@lexical/react/LexicalComposer";
      import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
      import { ContentEditable } from "@lexical/react/LexicalContentEditable";
      import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
      import {
        liveblocksConfig,
        LiveblocksPlugin,
        FloatingToolbar,
      } from "@liveblocks/react-lexical";
      import { Threads } from "./Threads";

      export function Editor() {
        // Wrap your Lexical config with `liveblocksConfig`
        const initialConfig = liveblocksConfig({
          namespace: "Demo",
          onError: (error: unknown) => {
            console.error(error);
            throw error;
          },
        });

        return (
          <LexicalComposer initialConfig={initialConfig}>
            <div className="editor">
              <RichTextPlugin
                contentEditable={<ContentEditable />}
                placeholder={<div className="placeholder">Start typing here…</div>}
                ErrorBoundary={LexicalErrorBoundary}
              />
              <LiveblocksPlugin>
                <Threads />
                <FloatingToolbar />
              </LiveblocksPlugin>
            </div>
          </LexicalComposer>
        );
      }
      ```
    </StepContent>

  </Step>
  <Step>
    <StepTitle>Render threads and composer</StepTitle>
    <StepContent>

      To add [Comments](/docs/ready-made-features/comments) to your text editor, we need to import a thread composer
      and list each thread on the page. Create a `Threads.tsx` file that uses [`FloatingComposer`](/docs/api-reference/liveblocks-react-lexical#FloatingComposer)
      for creating new threads, alongside [`AnchoredThreads`](/docs/api-reference/liveblocks-react-lexical#AnchoredThreads) and [`FloatingThreads`](/docs/api-reference/liveblocks-react-lexical#FloatingThreads) for displaying threads on desktop and mobile.

      ```tsx file="Threads.tsx"
      "use client";

      import { useThreads } from "@liveblocks/react/suspense";
      import {
        AnchoredThreads,
        FloatingComposer,
        FloatingThreads,
      } from "@liveblocks/react-lexical";

      export function Threads() {
        const { threads } = useThreads();

        return (
          <>
            <div className="anchored-threads">
              <AnchoredThreads threads={threads} />
            </div>
            <FloatingThreads className="floating-threads" threads={threads} />
            <FloatingComposer className="floating-composer" />
          </>
        );
      }
      ```
    </StepContent>

  </Step>
  <Step>
    <StepTitle>Style your editor</StepTitle>
    <StepContent>

      Lexical text editor is unstyled by default, so we can create some custom styles for it in a `globals.css` file. Import `globals.css`, alongside the default Liveblocks styles. You can import them into the
      root layout of your app or directly into a CSS file with `@import`.

      ```css file="globals.css" isCollapsed isCollapsable
      .editor {
        position: relative;
        display: flex;
        width: 100%;
        height: 100%;
      }

      [data-lexical-editor] {
        padding: 2px 12px;
        outline: none;
      }

      [data-lexical-editor] p {
        margin: 0.8em 0;
      }

      /* For mobile */
      .floating-threads {
        display: none;
      }

      /* For desktop */
      .anchored-threads {
        display: block;
        max-width: 300px;
        width: 100%;
        position: absolute;
        right: 4px;
      }

      @media (max-width: 640px) {
        .floating-threads {
          display: block;
        }

        .anchored-threads {
          display: none;
        }
      }

      .placeholder {
        position: absolute;
        left: 12px;
        top: 16px;
        pointer-events: none;
        opacity: 0.5;
      }
      ```

      ```tsx
      import "@liveblocks/react-ui/styles.css";
      import "@liveblocks/react-lexical/styles.css";
      import "./globals.css";
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Next: authenticate and add your users</StepTitle>
    <StepContent>
      Text Editor is set up and working now, but each user is anonymous—the next step is to
      authenticate each user as they connect, and attach their name, color, and avatar, to their cursors and mentions.

      <Button asChild  className="not-markdown">
        <a href="/docs/guides/how-to-add-users-to-liveblocks-text-editor">
          Add your users to Text Editor
        </a>
      </Button>
    </StepContent>

  </Step>
   <Step lastStep>
    <StepTitle>Optional: add more features</StepTitle>
    <StepContent>
      Lexical is a highly extensible text editor and it's possible to create complex rich-text applications.
      A great example is in the [Lexical playground](https://playground.lexical.dev/) which enables features
      such as tables, text highlights, embedded images, and more. This is all supported using Liveblocks.

      <Button asChild  className="not-markdown">
        <a href="https://lexical.dev/docs/intro">
          Learn more about Lexical
        </a>
      </Button>
    </StepContent>

  </Step>

</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Lexical text editor inside your React application.

- [Overview](/docs/ready-made-features/text-editor/lexical)
- [`@liveblocks/react-lexical` API Reference](/docs/api-reference/liveblocks-react-lexical)
- [`@liveblocks/node-lexical` API Reference](/docs/api-reference/liveblocks-node-lexical)
- [Lexical website](https://lexical.dev/)

---

## Examples using Lexical

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Text Editor",
      slug: "collaborative-text-editor/nextjs-lexical",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with an in-app notification inbox using Liveblocks and React"
  parentTitle: "Quickstart"
  description:
    "Learn how to add an in-app notification inbox using Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
notifications to your application using the hooks from
[`@liveblocks/react`](/docs/api-reference/liveblocks-react) and the components
from [`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui).

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      Liveblocks Notifications uses the concept of projects, which relate to
      projects in [your dashboard](/dashboard). Notifications are sent between
      users in the same project. To connect and receive notifications, you must
      add [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
      to a client component in your app.

      ```tsx file="App.tsx"
      "use client";

      import { LiveblocksProvider } from "@liveblocks/react/suspense";
      import { MyApp } from "./MyApp";

      export default function App() {
        return (
          // +++
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <MyApp />
          </LiveblocksProvider>
          // +++
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Use the Liveblocks hooks and components</StepTitle>
    <StepContent>

      Now that we’ve set up the provider, we can start using the Liveblocks hooks and components.
      We’ll add [`useInboxNotifications`](/docs/api-reference/liveblocks-react#useInboxNotifications)
      to get the current project’s notifications, then we’ll
      use [`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification) and [`InboxNotificationList`](/docs/api-reference/liveblocks-react-ui#InboxNotificationList) to render them.

      ```tsx file="app/MyApp.tsx"
      "use client";

      import { useInboxNotifications } from "@liveblocks/react/suspense";
      import {
        InboxNotification,
        InboxNotificationList,
      } from "@liveblocks/react-ui";

      export function MyApp() {
        // +++
        const { inboxNotifications } = useInboxNotifications();
        // +++

        return (
          // +++
          <InboxNotificationList>
            {inboxNotifications.map((inboxNotification) => (
              <InboxNotification
                key={inboxNotification.id}
                inboxNotification={inboxNotification}
              />
            ))}
          </InboxNotificationList>
          // +++
        );
      }
      ```

    </StepContent>

  </Step>
   <Step>
    <StepTitle>Import default styles</StepTitle>
    <StepContent>

    The default components come with default styles, you can import them into the
    root of your app or directly into a CSS file with `@import`.

    ```tsx
    import "@liveblocks/react-ui/styles.css";
    ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: authenticate and add your users</StepTitle>
    <StepContent>

      Notifications is set up and working now, but each user is anonymous—the next step is to
      authenticate each user as they connect, and attach their name and avatar to their notifications.

      <Button asChild className="not-markdown">
        <a href="/docs/guides/how-to-add-users-to-liveblocks-notifications">
          Add your users to Notifications
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You’ve set up the foundation to start building a notifications
experience for your React application.

- [API reference](/docs/api-reference/liveblocks-react#Notifications)
- [Component reference](/docs/api-reference/liveblocks-react-ui#Notifications)

---

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "comments-notifications/nextjs-comments-notifications",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom/nextjs-notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notification settings",
      slug: "notification-settings/nextjs-notification-settings",
      image: "/images/examples/thumbnails/notification-settings.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI Editor",
      slug: "notion-like-ai-editor/nextjs-notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with a Tiptap text editor using Liveblocks and React"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Tiptap text editor using Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your React application using the APIs from the
[`@liveblocks/react-tiptap`](/docs/api-reference/liveblocks-react-tiptap)
package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks and Tiptap</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/react-tiptap @tiptap/react @tiptap/starter-kit
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. Set up a Liveblocks client with [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), and join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider).

      ```tsx file="App.tsx" highlight="11-15"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              {/* ... */}
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      After setting up the room, you can add collaborative components inside it, using
      [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense) to add loading spinners to your app.

      ```tsx file="App.tsx" highlight="14-16"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                <Editor />
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the collaborative Tiptap text editor</StepTitle>
    <StepContent>

      Now that we set up Liveblocks, we can start integrating Tiptap and Liveblocks in the `Editor.tsx` file.
    To make the editor collaborative, we can add
    [`useLiveblocksExtension`](/docs/api-reference/liveblocks-react-tiptap#useLiveblocksExtension)
    from `@liveblocks/react-tiptap`. [`FloatingToolbar`](/docs/api-reference/liveblocks-react-tiptap#FloatingToolbar)
      adds a text selection toolbar.

    ```tsx file="Editor.tsx"
    "use client";

    import { useLiveblocksExtension, FloatingToolbar } from "@liveblocks/react-tiptap";
    import { useEditor, EditorContent } from "@tiptap/react";
    import StarterKit from "@tiptap/starter-kit";
    import { Threads } from "./Threads";

    export function Editor() {
      const liveblocks = useLiveblocksExtension();

      const editor = useEditor({
        extensions: [
          liveblocks,
          StarterKit.configure({
            // The Liveblocks extension comes with its own history handling
            undoRedo: false,
          }),
        ],
      });

      return (
        <div>
          <EditorContent editor={editor} className="editor" />
          <Threads editor={editor} />
          <FloatingToolbar editor={editor} />
        </div>
      );
    }
    ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Render threads and composer</StepTitle>
    <StepContent>

     To add [Comments](/docs/ready-made-features/comments) to your text editor, we need to import a thread composer
    and list each thread on the page. Create a `Threads.tsx` file that uses [`FloatingComposer`](/docs/api-reference/liveblocks-react-tiptap#FloatingComposer)
    for creating new threads, alongside [`AnchoredThreads`](/docs/api-reference/liveblocks-react-tiptap#AnchoredThreads) and [`FloatingThreads`](/docs/api-reference/liveblocks-react-tiptap#FloatingThreads) for displaying threads on desktop and mobile.

    ```tsx file="Threads.tsx"
    import { useThreads } from "@liveblocks/react/suspense";
    import {
      AnchoredThreads,
      FloatingComposer,
      FloatingThreads,
    } from "@liveblocks/react-tiptap";
    import { Editor } from "@tiptap/react";

    export function Threads({ editor }: { editor: Editor | null }) {
      const { threads } = useThreads({ query: { resolved: false } });

      return (
        <>
          <div className="anchored-threads">
            <AnchoredThreads editor={editor} threads={threads} />
          </div>
          <FloatingThreads
            editor={editor}
            threads={threads}
            className="floating-threads"
          />
          <FloatingComposer editor={editor} className="floating-composer" />
        </>
      );
    }
    ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Style your editor</StepTitle>
    <StepContent>

      Tiptap text editor is unstyled by default, so we can create some custom styles for it in a `globals.css` file. Import `globals.css`, alongside the default Liveblocks styles. You can import them into the
    root layout of your app or directly into a CSS file with `@import`.

    ```css file="app/globals.css" isCollapsed isCollapsable
    .editor {
      position: relative;
      display: flex;
      width: 100%;
      height: 100%;
    }

    .tiptap {
      padding: 2px 12px;
      outline: none;
      width: 100%;
    }

    /* For mobile */
    .floating-threads {
      display: none;
    }

    /* For desktop */
    .anchored-threads {
      display: block;
      max-width: 300px;
      width: 100%;
      position: absolute;
      right: 12px;
    }

    @media (max-width: 640px) {
      .floating-threads {
        display: block;
      }

      .anchored-threads {
        display: none;
      }
    }
    ```

    ```tsx file="app/layout.tsx"
    import "@liveblocks/react-ui/styles.css";
    import "@liveblocks/react-tiptap/styles.css";
    import "./globals.css";
    ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Next: authenticate and add your users</StepTitle>
    <StepContent>
      Text Editor is set up and working now, but each user is anonymous—the next step is to
      authenticate each user as they connect, and attach their name, color, and avatar, to their cursors and mentions.

      <Button asChild  className="not-markdown">
        <a href="/docs/guides/how-to-add-users-to-liveblocks-text-editor">
          Add your users to Text Editor
        </a>
      </Button>
    </StepContent>

  </Step>
   <Step lastStep>
    <StepTitle>Optional: add more features</StepTitle>
    <StepContent>
      Tiptap is easy to extend, and a number of extensions are already available, making it possible to quickly create complex rich-text applications. For example you can enable features
    such as tables, text highlights, embedded images, and more. This is all supported using Liveblocks.

    <Button asChild  className="not-markdown">
      <a href="https://tiptap.dev/docs/editor/extensions/overview">
        Find more extensions
      </a>
    </Button>

    </StepContent>

  </Step>

</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Tiptap text editor inside your React application.

- [@liveblocks/react-tiptap API Reference](/docs/api-reference/liveblocks-react-tiptap)
- [Tiptap guides](/docs/guides?technologies=tiptap)
- [Tiptap website](https://tiptap.dev)

---

## Examples using Tiptap

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Text Editor",
      slug: "collaborative-text-editor/nextjs-tiptap",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Advanced Collaborative Text Editor",
      slug: "collaborative-text-editor-advanced/nextjs-tiptap-advanced",
      image: "/images/examples/thumbnails/text-editor-advanced.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with Liveblocks and React"
  parentTitle: "Quickstart"
  description: "Learn how to get started with Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your React application using the hooks from the
[`@liveblocks/react`](/docs/api-reference/liveblocks-react) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. Set up a Liveblocks client with [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), and join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider).

      ```tsx file="App.tsx" highlight="11-15"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
      } from "@liveblocks/react/suspense";
      import { Room } from "./Room";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              {/* ... */}
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      After setting up the room, you can add collaborative components inside it, using
      [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense) to add loading spinners to your app.

      ```tsx file="App.tsx" highlight="14-16"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";
      import { Room } from "./Room";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                <Room />
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Use the Liveblocks hooks</StepTitle>
    <StepContent>

      Now that we’re connected to a room, we can start using the Liveblocks hooks.
      The first we’ll add is [`useOthers`](/docs/api-reference/liveblocks-react#useOthers), a hook that provides information about
      which other users are connected to the room.

      ```tsx file="Room.tsx" highlight="6"
      "use client";

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

      export function Room() {
        const others = useOthers();
        const userCount = others.length;

        return <div>There are {userCount} other user(s) online</div>;
      }
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>

      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation to start building
collaborative experiences for your React application.

- [@liveblocks/react API Reference](/docs/api-reference/liveblocks-react)
- [React guides](/docs/guides?technologies=react)
- [How to use Liveblocks Presence with React](/docs/guides/how-to-use-liveblocks-presence-with-react)
- [How to use Liveblocks Storage with React](/docs/guides/how-to-use-liveblocks-storage-with-react)

---

## Examples using React

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative To-do List",
      slug: "collaborative-todo-list/nextjs-todo-list",
      image: "/images/examples/thumbnails/collaborative-todo-list.jpg",
    }}
    technologies={["nextjs", "vuejs", "sveltekit", "solidjs", "javascript"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Live Avatar Stack",
      slug: "live-avatar-stack/nextjs-live-avatars",
      image: "/images/examples/thumbnails/live-avatar-stack.jpg",
    }}
    technologies={["nextjs", "nuxtjs", "vuejs", "sveltekit", "solidjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Spreadsheet",
      slug: "collaborative-spreadsheet-advanced/nextjs-spreadsheet-advanced",
      image:
        "/images/examples/thumbnails/collaborative-spreadsheet-advanced.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Whiteboard",
      slug: "collaborative-whiteboard-advanced/nextjs-whiteboard-advanced",
      image:
        "/images/examples/thumbnails/collaborative-whiteboard-advanced.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with Liveblocks and Redux"
  parentTitle: "Quickstart"
  description: "Learn how to get started with Liveblocks and Redux"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start making your Redux
state multiplayer by using the
[store enhancer](https://redux.js.org/understanding/thinking-in-redux/glossary#store-enhancer)
from the [`@liveblocks/redux`](/docs/api-reference/liveblocks-redux) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/redux
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Connect your Redux store to Liveblocks</StepTitle>
    <StepContent>

      Create the Liveblocks client and use the
      `liveblocksEnhancer` in your Redux store setup. This will
      add a new state called `liveblocks` to your store, enabling
      you to interact with our Presence and Storage APIs.

      ```ts file="store.ts" highlight="7-9,24-25"
      "use client";

      import { createClient } from "@liveblocks/client";
      import { liveblocksEnhancer } from "@liveblocks/redux";
      import { configureStore, createSlice } from "@reduxjs/toolkit";

      const client = createClient({
        publicApiKey: "pk_prod_xxxxxxxxxxxxxxxxxxxxxxxx",
      });

      const initialState = {};

      const slice = createSlice({
        name: "state",
        initialState,
        reducers: {
          /* logic will be added here */
        },
      });

      function makeStore() {
        return configureStore({
          reducer: slice.reducer,
          enhancers: (getDefaultEnhancers) =>
            getDefaultEnhancers().concat(liveblocksEnhancer({ client })),
        });
      }

      const store = makeStore();

      export default store;
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people collaborate.
      To create a realtime experience, multiple users must be connected to the same room.

      ```tsx file="App.tsx" highlight="5,11,14"
      "use client";

      import { useEffect } from "react";
      import { useDispatch } from "react-redux";
      import { actions } from "@liveblocks/redux";

      export default function App() {
        const dispatch = useDispatch();

        useEffect(() => {
          dispatch(actions.enterRoom("room-id"));

          return () => {
            dispatch(actions.leaveRoom("room-id"));
          };
        }, [dispatch]);

        return <Room />;
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Use the Liveblocks data from the store</StepTitle>
    <StepContent>

      Now that we’re connected to a room, we can start using the Liveblocks data from the Redux store.

      ```tsx file="Room.tsx" highlight="6"
      "use client";

      import { useSelector } from "react-redux";

      export function Room() {
        const others = useSelector((state) => state.liveblocks.others);
        const userCount = others.length;
        return <div>There are {userCount} other user(s) online</div>;
      }
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>

      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation to start building
collaborative experiences for your Redux store.

- [@liveblocks/redux API Reference](/docs/api-reference/liveblocks-redux)
- [Redux guides](/docs/guides?technologies=redux)
- [How to create a collaborative online whiteboard with Redux](/docs/guides/how-to-create-a-collaborative-online-whiteboard-with-react-redux-and-liveblocks)
- [How to create a collaborative to-do list with Redux](/docs/guides/how-to-create-a-collaborative-to-do-list-with-react-redux-and-liveblocks)

---

## Examples using Redux

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative To-do List",
      slug: "collaborative-todo-list/redux-todo-list",
      image: "/images/examples/thumbnails/collaborative-todo-list.jpg",
    }}
    technologies={["nextjs", "vuejs", "sveltekit", "solidjs", "javascript"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Whiteboard",
      slug: "collaborative-whiteboard/redux-whiteboard",
      image: "/images/examples/thumbnails/collaborative-whiteboard.jpg",
      advanced: true,
    }}
    technologies={["nextjs", "redux", "zustand"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with Liveblocks and SolidJS"
  parentTitle: "Quickstart"
  description: "Learn how to get started with Liveblocks and SolidJS"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your SolidJS application using the APIs from the
[`@liveblocks/client`](/docs/api-reference/liveblocks-client) package.

<Banner title="Official package">

Liveblocks does not have a package for SolidJS. If you would like to have one,
or even better if you have ideas about what kind of API you would like to use,
please let us know about it on this
[GitHub issue](https://github.com/liveblocks/liveblocks/issues/672).

</Banner>

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      The first step in connecting to Liveblocks is creating a client which
      will be responsible for communicating with the back end.

      ```ts file="room.js"
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people collaborate.
      To create a realtime experience, multiple users must be connected to the same room.

      ```js
      const { room, leave } = client.enterRoom("my-room");
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Use the Liveblocks methods</StepTitle>
    <StepContent>

      Now that we’re connected to a room, we can start using Liveblocks subscriptions.
      The first we’ll add is `others`, a subscription that provides information
      about which other users are connected to the room.

      ```js highlight="8-10"
      import { createSignal, onCleanup, onMount } from "solid-js";
      import { room } from "./room.js";

      export function Room() {
        const [others, setOthers] = createSignal(room.getOthers());

        onMount(() => {
          const unsubscribeOthers = room.subscribe("others", (updatedOthers) => {
            setOthers(updatedOthers);
          });

          onCleanup(() => {
            unsubscribeOthers();
          });
        });

        return (
          <div>There are {others.length} other user(s) online</div>
        );
      }
      ```

    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>

      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation to start building
collaborative experiences for your SolidJS application.

- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)

---

## Examples using SolidJS

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Live Avatar Stack",
      slug: "live-avatar-stack/solidjs-live-avatars",
      image: "/images/examples/thumbnails/live-avatar-stack.jpg",
    }}
    technologies={["nextjs", "nuxtjs", "vuejs", "sveltekit", "solidjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Live Cursors",
      slug: "live-cursors/solidjs-live-cursors",
      image: "/images/examples/thumbnails/live-cursors.jpg",
    }}
    technologies={["nextjs", "nuxtjs", "vuejs", "sveltekit", "solidjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with Liveblocks and Svelte"
  parentTitle: "Quickstart"
  description: "Learn how to get started with Liveblocks and Svelte"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Svelte application using the APIs from the
[`@liveblocks/client`](/docs/api-reference/liveblocks-client) package.

<Banner title="Official package">

Liveblocks does not have a package for Svelte. If you would like to have one, or
even better if you have ideas about what kind of API you would like to use,
please let us know on
[GitHub](https://github.com/liveblocks/liveblocks/issues/1).

</Banner>

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      The first step in connecting to Liveblocks is creating a client which
      will be responsible for communicating with the back end.

      ```ts file="room.js"
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people collaborate.
      To create a realtime experience, multiple users must be connected to the same room.

      ```js
      const { room, leave } = client.enterRoom("my-room");
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Use the Liveblocks methods</StepTitle>
    <StepContent>

      Now that we’re connected to a room, we can start using Liveblocks subscriptions.
      The first we’ll add is `others`, a subscription that provides information
      about which other users are connected to the room.

      ```js highlight="7-9"
      <script>
        import { onDestroy } from "svelte";
        import { room } from "./room.js";

        let others = room.getOthers();

        const unsubscribeOthers = room.subscribe("others", (updatedOthers) => {
          others = updatedOthers;
        });

        onDestroy(() => {
          unsubscribeOthers();
        });
      </script>

      <div>There are {others.length} other user(s) online</div>
      ```

    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>

      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation to start building
collaborative experiences for your Svelte application.

- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)

---

## Examples using Svelte

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Live Cursors",
      slug: "live-cursors/sveltekit-live-cursors",
      image: "/images/examples/thumbnails/live-cursors.jpg",
    }}
    technologies={["nextjs", "vuejs", "sveltekit", "solidjs", "javascript"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Live Avatar Stack",
      slug: "live-avatar-stack/sveltekit-live-avatars",
      image: "/images/examples/thumbnails/live-avatar-stack.jpg",
    }}
    technologies={["nextjs", "nuxtjs", "vuejs", "sveltekit", "solidjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with Liveblocks and Vue.js"
  parentTitle: "Quickstart"
  description: "Learn how to get started with Liveblocks and Vue.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Vue.js application using the APIs from the
[`@liveblocks/client`](/docs/api-reference/liveblocks-client) package.

<Banner title="Official package">

Liveblocks does not have a package for Vue.js. If you would like to have one, or
even better if you have ideas about what kind of API you would like to use,
please let us know about it on this
[GitHub issue](https://github.com/liveblocks/liveblocks/issues/1).

</Banner>

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      The first step in connecting to Liveblocks is creating a client which
      will be responsible for communicating with the back end.

      ```ts file="room.js"
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people collaborate.
      To create a realtime experience, multiple users must be connected to the same room.

      ```js
      const { room, leave } = client.enterRoom("my-room");
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Use the Liveblocks methods</StepTitle>
    <StepContent>

      Now that we’re connected to a room, we can start using Liveblocks subscriptions.
      The first we’ll add is `others`, a subscription that provides information
      about which other users are connected to the room.

      ```js highlight="7-9"
      <script setup>
        import { onUnmounted, ref } from "vue";
        import { room } from "./room.js";

        const others = ref(room.getOthers());

        const unsubscribeOthers = room.subscribe("others", (updatedOthers) => {
          others.current = updatedOthers;
        });

        onUnmounted(() => {
          unsubscribeOthers();
        });
      </script>

      <template>
        <div>There are {{ others.length }} other user(s) online</div>
      </template>
      ```

    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>

      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation to start building
collaborative experiences for your Vue.js application.

- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)

---

## Examples using Vue.js

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Live Avatar Stack",
      slug: "live-avatar-stack/vuejs-live-avatars",
      image: "/images/examples/thumbnails/live-avatar-stack.jpg",
    }}
    technologies={["nextjs", "nuxtjs", "vuejs", "sveltekit", "solidjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Live Cursors",
      slug: "live-cursors/vuejs-live-cursors",
      image: "/images/examples/thumbnails/live-cursors.jpg",
    }}
    technologies={["nextjs", "nuxtjs", "vuejs", "sveltekit", "solidjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title:
    "Get started with a CodeMirror code editor using Liveblocks and JavaScript"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a CodeMirror code editor using Liveblocks and
    JavaScript"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your JavaScript application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and CodeMirror</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Create an HTML element</StepTitle>
    <StepContent>

      ```html
      <div id="editor"></div>
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative CodeMirror code editor</StepTitle>
    <StepContent>

      ```js file="app.js"
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import { yCollab } from "y-codemirror.next";
      import { EditorView, basicSetup } from "codemirror";
      import { EditorState } from "@codemirror/state";
      import { javascript } from "@codemirror/lang-javascript";

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      // Enter a multiplayer room
      const { room, leave } = client.enterRoom("my-room");

      // Set up Yjs document, shared text, and Liveblocks Yjs provider
      const yProvider = getYjsProviderForRoom(room);
      const yDoc = yProvider.getYDoc()
      const yText = yDoc.getText("codemirror");

      // Set up CodeMirror and extensions
      const state = EditorState.create({
        doc: yText.toString(),
        extensions: [
          basicSetup,
          javascript(),
          yCollab(yText, yProvider.awareness, { undoManager }),
        ],
      });

      // Attach CodeMirror to element
      const parent = document.querySelector("#editor");
      view = new EditorView({
        state,
        parent,
      });
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
CodeMirror code editor inside your JavaScript application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [CodeMirror website](https://codemirror.net)

---
meta:
  title: "Get started with a CodeMirror code editor using Liveblocks and React"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a CodeMirror code editor using Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your React application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and CodeMirror</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. Set up a Liveblocks client with [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), and join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider).

      ```tsx file="App.tsx" highlight="11-15"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              {/* ... */}
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      After setting up the room, you can add collaborative components inside it, using
      [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense) to add loading spinners to your app.

      ```tsx file="App.tsx" highlight="14-16"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                <Editor />
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the collaborative CodeMirror editor</StepTitle>
    <StepContent>

      Now that we set up Liveblocks, we can start integrating CodeMirror and Yjs in the `Editor.tsx` file.
      To make the editor collaborative, we can rely on the `yCollab` from `y-codemirror.next`.

      ```tsx
      "use client";

      import * as Y from "yjs";
      import { yCollab } from "y-codemirror.next";
      import { EditorView, basicSetup } from "codemirror";
      import { EditorState } from "@codemirror/state";
      import { javascript } from "@codemirror/lang-javascript";
      import { useCallback, useEffect, useState } from "react";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import { useRoom } from "@/liveblocks.config";
      import styles from "./Editor.module.css";

      export default function Editor() {
        const room = useRoom();
        const yProvider = getYjsProviderForRoom(room);
        const [element, setElement] = useState<HTMLElement>();

        const ref = useCallback((node: HTMLElement | null) => {
          if (!node) return;

          setElement(node);
        }, []);

        // Set up Liveblocks Yjs provider and attach CodeMirror editor
        useEffect(() => {
          let view: EditorView;

          if (!element || !room) {
            return;
          }

          // Get document
          const yDoc = yProvider.getYDoc();
          const yText = yDoc.getText("codemirror");
          const undoManager = new Y.UndoManager(yText);

          // Set up CodeMirror and extensions
          const state = EditorState.create({
            doc: yText.toString(),
            extensions: [
              basicSetup,
              javascript(),
              yCollab(yText, yProvider.awareness, { undoManager }),
            ],
          });

          // Attach CodeMirror to element
          view = new EditorView({
            state,
            parent: element,
          });

          return () => {
            view?.destroy();
          };
        }, [element, room]);

        return <div ref={ref} className={styles.editor} />;
      }
      ```
    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>

      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
CodeMirror editor inside your React application.

- [Yjs and CodeMirror guides](/docs/guides?technologies=yjs%2Ccodemirror)
- [How to create a collaborative code editor with CodeMirror, Yjs, Next.js, and Liveblocks](/docs/guides/how-to-create-a-collaborative-code-editor-with-codemirror-yjs-nextjs-and-liveblocks)
- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [CodeMirror website](https://codemirror.net)

---

## Examples using CodeMirror

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Code Editor",
      slug: "collaborative-code-editor/nextjs-yjs-codemirror",
      image: "/images/examples/thumbnails/code-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with a CodeMirror code editor using Liveblocks and Svelte"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a CodeMirror code editor using Liveblocks and Svelte"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Svelte application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and CodeMirror</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative CodeMirror code editor</StepTitle>
    <StepContent>

      ```html file="Editor.svelte"
      <script>
      import { onMount, onDestroy } from "svelte";
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import { yCollab } from "y-codemirror.next";
      import { EditorView, basicSetup } from "codemirror";
      import { EditorState } from "@codemirror/state";
      import { javascript } from "@codemirror/lang-javascript";

      let parent;

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      onMount(() => {
        // Enter a multiplayer room
        const { room, leave } = client.enterRoom("my-room");

        // Set up Yjs document, shared text, undo manager, and Liveblocks Yjs provider
        const yProvider = getYjsProviderForRoom(room);
        const yDoc = yProvider.getYDoc();
        const yText = yDoc.getText("codemirror");
        const undoManager = new Y.UndoManager(yText);

        // Set up CodeMirror and extensions
        const state = EditorState.create({
          doc: yText.toString(),
          extensions: [
            basicSetup,
            javascript(),
            yCollab(yText, yProvider.awareness, { undoManager }),
          ],
        });

        // Attach CodeMirror to element
        const view = new EditorView({
          state,
          parent,
        });

        return () => {
          view.destroy();
          leave();
        };
      });
      </script>

      <div bind:this={parent}></div>
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
CodeMirror code editor inside your Svelte application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [CodeMirror website](https://codemirror.net)

---
meta:
  title: "Get started with a CodeMirror code editor using Liveblocks and Vue.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a CodeMirror code editor using Liveblocks and Vue.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Vue.js application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and CodeMirror</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative CodeMirror code editor</StepTitle>
    <StepContent>

      ```html file="Editor.vue"
      <script setup>
      import { ref, onMounted, onUnmounted } from "vue";
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import { yCollab } from "y-codemirror.next";
      import { EditorView, basicSetup } from "codemirror";
      import { EditorState } from "@codemirror/state";
      import { javascript } from "@codemirror/lang-javascript";

      const parent = ref(null);
      const leave = ref(null);
      const view = ref(null);

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      // Enter a multiplayer room
      const info = client.enterRoom("my-room");
      const room = info.room;
      leave.value = info.leave;

      // Set up Yjs document, shared text, and Liveblocks Yjs provider
      const yProvider = getYjsProviderForRoom(room);
      const yDoc = yProvider.getYDoc();
      const yText = yDoc.getText("codemirror");

      onMounted(() => {
        // Set up CodeMirror and extensions
        const state = EditorState.create({
          doc: yText.toString(),
          extensions: [
            basicSetup,
            javascript(),
            yCollab(yText, yProvider.awareness, { undoManager }),
          ],
        });

        // Attach CodeMirror to element
        view.current = new EditorView({
          state,
          parent: parent.value,
        });
      });

      onUnmounted(() => {
        view?.destroy();
        leave.value?.();
      });
      </script>

      <template>
        <div ref="parent" />
      </template>
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
CodeMirror code editor inside your Vue.js application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [CodeMirror website](https://codemirror.net)

---
meta:
  title: "Get started with a Monaco code editor using Liveblocks and JavaScript"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Monaco code editor using Liveblocks and JavaScript"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your JavaScript application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Monaco</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs monaco-editor y-monaco
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Create an HTML element</StepTitle>
    <StepContent>

      ```html
      <div id="editor"></div>
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative Monaco code editor</StepTitle>
    <StepContent>

      ```js file="app.js"
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import * as monaco from "monaco-editor";
      import { MonacoBinding } from "y-monaco";

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      // Enter a multiplayer room
      const { room, leave } = client.enterRoom("my-room");

      // Set up Yjs document, shared text, and Liveblocks Yjs provider
      const yProvider = getYjsProviderForRoom(room);
      const yDoc = yProvider.getYDoc();
      const yText = yDoc.getText("monaco");

      // Set up the Monaco editor
      const parent = document.querySelector("#editor");
      const editor = monaco.editor.create(parent, {
        value: "",
        language: "javascript"
      });

      // Attach Yjs to Monaco
      const monacoBinding = new MonacoBinding(
        yText,
        editor.getModel(),
        new Set([editor]),
        yProvider.awareness
      );
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Monaco code editor inside your JavaScript application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [Monaco website](https://microsoft.github.io/monaco-editor/)

---
meta:
  title: "Get started with a Monaco code editor using Liveblocks and React"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Monaco code editor using Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your React application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Monaco</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs @monaco-editor/react y-monaco y-protocols
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. Set up a Liveblocks client with [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), and join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider).

      ```tsx file="App.tsx"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          // +++
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              {/* ... */}
            </RoomProvider>
          </LiveblocksProvider>
          // +++
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      After setting up the room, you can add collaborative components inside it, using
      [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense) to add loading spinners to your app.

      ```tsx file="App.tsx" highlight="14-16"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";
      import { CollaborativeEditor } from "./CollaborativeEditor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                <CollaborativeEditor />
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the collaborative Monaco code editor</StepTitle>
    <StepContent>

      Now that we set up Liveblocks, we can start integrating Monaco and Yjs in the `Editor.tsx` file.
      To make the editor collaborative, we can rely on `MonacoBinding` from `y-monaco`.

      ```tsx file="CollaborativeEditor.tsx"
      "use client";

      import * as Y from "yjs";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import { useRoom } from "@/liveblocks.config";
      import { useCallback, useEffect, useState } from "react";
      import { Editor } from "@monaco-editor/react";
      import { editor } from "monaco-editor";
      import { MonacoBinding } from "y-monaco";
      import { Awareness } from "y-protocols/awareness";

      // Collaborative text editor with simple rich text, live cursors, and live avatars
      export function CollaborativeEditor() {
        const [editorRef, setEditorRef] = useState<editor.IStandaloneCodeEditor>();
        const room = useRoom();
        const yProvider = getYjsProviderForRoom(room);

        // Set up Liveblocks Yjs provider and attach Monaco editor
        useEffect(() => {
          let binding: MonacoBinding;

          if (editorRef) {
            const yDoc = yProvider.getYDoc();
            const yText = yDoc.getText("monaco");

            // Attach Yjs to Monaco
            binding = new MonacoBinding(
              yText,
              editorRef.getModel() as editor.ITextModel,
              new Set([editorRef]),
              yProvider.awareness as Awareness
            );
          }

          return () => {
            binding?.destroy();
          };
        }, [editorRef, room]);

        const handleOnMount = useCallback((e: editor.IStandaloneCodeEditor) => {
          setEditorRef(e);
        }, []);

        return (
          <Editor
            onMount={handleOnMount}
            height="100vh"
            width="100%"
            theme="vs-light"
            defaultLanguage="typescript"
            defaultValue=""
            options={{
              tabSize: 2,
            }}
          />
        );
      }
      ```

    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>

</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Monaco code editor inside your React application.

- [Yjs and Monaco guides](/docs/guides?technologies=yjs%2Cmonaco)
- [How to create a collaborative code editor with Monaco, Yjs, Next.js, and Liveblocks](/docs/guides/how-to-create-a-collaborative-code-editor-with-monaco-yjs-nextjs-and-liveblocks)
- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [Monaco website](https://microsoft.github.io/monaco-editor/)

---

## Examples using Monaco

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Code Editor",
      slug: "collaborative-code-editor/nextjs-yjs-monaco",
      image: "/images/examples/thumbnails/code-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with a Monaco code editor using Liveblocks and Svelte"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Monaco code editor using Liveblocks and Svelte"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Svelte application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Monaco</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs monaco-editor y-monaco
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative Monaco code editor</StepTitle>
    <StepContent>

      ```html file="Editor.svelte"
      <script>
      import { onMount, onDestroy } from "svelte";
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import * as monaco from "monaco-editor";
      import { MonacoBinding } from "y-monaco";

      let element;
      const roomId = "my-room";

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      onMount(() => {
        // Enter a multiplayer room
        const { room, leave } = client.enterRoom(roomId);

        // Set up Yjs document, shared text, and Liveblocks Yjs provider
        const yProvider = getYjsProviderForRoom(room);
        const yDoc = yProvider.getYDoc();
        const yText = yDoc.getText("monaco");

        // Set up the Monaco editor
        const editor = monaco.editor.create(element, {
          value: "",
          language: "javascript"
        });

        // Attach Yjs to Monaco
        const binding = new MonacoBinding(
          yText,
          editor.getModel(),
          new Set([editor]),
          yProvider.awareness
        );

        return () => {
          binding.destroy();
          editor.dispose();
          leave();
        };
      });

      </script>

      <div bind:this={element}></div>
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Monaco code editor inside your Svelte application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [Monaco website](https://microsoft.github.io/monaco-editor/)

---
meta:
  title: "Get started with a Monaco code editor using Liveblocks and Vue.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Monaco code editor using Liveblocks and Vue.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Vue.js application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Monaco</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs monaco-editor y-monaco
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative Monaco code editor</StepTitle>
    <StepContent>

      ```html file="Editor.vue"
      <script setup>
      import { ref, onMounted, onUnmounted } from "vue";
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import * as monaco from "monaco-editor";
      import { MonacoBinding } from "y-monaco";

      const element = ref(null);
      const editor = ref(null);
      const binding = ref(null);
      const leave = ref(null);

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      // Enter a multiplayer room
      const info = client.enterRoom("my-room");
      const room = info.room;
      leave.value = info.leave;

      // Set up Yjs document, shared text, and Liveblocks Yjs provider
      const yProvider = getYjsProviderForRoom(room);
      const yDoc = yProvider.getYDoc();
      const yText = yDoc.getText("monaco");

      onMounted(() => {
        // Set up the Monaco editor
        editor.value = monaco.editor.create(element.value, {
          value: "",
          language: "javascript"
        })

        // Attach Yjs to Monaco
        binding.value = new MonacoBinding(
          yText,
          editor.value.getModel(),
          new Set([editor]),
          yProvider.awareness
        );
      });

      onUnmounted(() => {
        binding?.destroy();
        editor.value?.dispose();
        leave?.();
      });
      </script>

      <template>
        <div ref="element" />
      </template>
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Monaco code editor inside your Vue.js application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [Monaco website](https://microsoft.github.io/monaco-editor/)

---
meta:
  title: "Get started with a Quill text editor using Liveblocks and JavaScript"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Quill text editor using Liveblocks and JavaScript"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your JavaScript application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Quill</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs quill quill-cursors y-quill
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Create an HTML element</StepTitle>
    <StepContent>

      ```html
      <div id="editor"></div>
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative Quill text editor</StepTitle>
    <StepContent>

      ```js file="app.js"
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import Quill from "quill";
      import { QuillBinding } from "y-quill";
      import QuillCursors from "quill-cursors";

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      // Enter a multiplayer room
      const { room, leave } = client.enterRoom("my-room");

      // Set up Yjs document, shared text, and Liveblocks Yjs provider
      const yProvider = getYjsProviderForRoom(room);
      const yDoc = yProvider.getYDoc();
      const yText = yDoc.getText("quill");

      // Attach cursors plugin
      Quill.register("modules/cursors", QuillCursors);

      // Set up Quill editor and modules
      const parent = document.querySelector("#editor");
      const quill = new Quill(parent, {
        placeholder: "Start collaborating…",
        theme: "snow",
        modules: {
          cursors: true,
          toolbar: [
            [{ header: [1, 2, false] }],
            ["bold", "italic", "underline"],
            ["code-block"],
          ],
          history: {
            // Local undo shouldn’t undo changes made by other users
            userOnly: true,
          },
        },
      });

      // Attach Yjs to Quill
      const binding = new QuillBinding(yText, quill, yProvider.awareness);
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative Quill
text editor inside your JavaScript application.

- [Yjs and Quill guides](/docs/guides?technologies=yjs%2Cquill)
- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [Quill website](https://quilljs.com)

---
meta:
  title: "Get started with a Quill text editor using Liveblocks and React"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Quill text editor using Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your React application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Quill</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs quill quill-cursors react-quill y-quill
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. Set up a Liveblocks client with [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), and join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider).

      ```tsx file="App.tsx" highlight="11-15"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              {/* ... */}
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      After setting up the room, you can add collaborative components inside it, using
      [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense) to add loading spinners to your app.

      ```tsx file="App.tsx" highlight="14-16"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                <Editor />
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the collaborative Quill text editor</StepTitle>
    <StepContent>

      Now that we set up Liveblocks, we can start integrating Quill and Yjs in the `Editor.tsx` file.
      To make the editor collaborative, we can rely on `QuillBinding` and `QuillCursors`
      from `y-quill` and `quill-cursors`.

      ```tsx file="Editor.tsx"
      "use client";

      import Quill from "quill";
      import ReactQuill from "react-quill";
      import QuillCursors from "quill-cursors";
      import { QuillBinding } from "y-quill";
      import * as Y from "yjs";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import { useRoom } from "@/liveblocks.config";
      import { useCallback, useEffect, useRef, useState } from "react";

      Quill.register("modules/cursors", QuillCursors);

      // Collaborative text editor with simple rich text, live cursors, and live avatars
      export function CollaborativeEditor() {
        const room = useRoom();
        const yProvider = getYjsProviderForRoom(room);
        const yDoc = yProvider.getYDoc();
        const yText = yDoc.getText("quill");

        return <QuillEditor yText={yText} provider={yProvider} />;
      }

      type EditorProps = {
        yText: Y.Text;
        provider: any;
      };

      function QuillEditor({ yText, provider }: EditorProps) {
        const reactQuillRef = useRef<ReactQuill>(null);

        // Set up Yjs and Quill
        useEffect(() => {
          let quill;
          let binding: QuillBinding;

          if (!reactQuillRef.current) {
            return;
          }

          quill = reactQuillRef.current.getEditor();
          binding = new QuillBinding(yText, quill, provider.awareness);
          return () => {
            binding?.destroy?.();
          };
        }, [yText, provider]);

        return (
          <ReactQuill
            placeholder="Start typing here…"
            ref={reactQuillRef}
            theme="snow"
            modules={{
              cursors: true,
              history: {
                // Local undo shouldn't undo changes from remote users
                userOnly: true,
              },
            }}
          />
        );
      }
      ```

    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>

</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative Quill
text editor inside your React application.

- [Yjs and Quill guides](/docs/guides?technologies=yjs%2Cquill)
- [How to create a collaborative text editor with Quill, Yjs, Next.js, and Liveblocks](/docs/guides/how-to-create-a-collaborative-text-editor-with-quill-yjs-nextjs-and-liveblocks)
- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [Quill website](https://quilljs.com)

---

## Examples using Quill

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Text Editor",
      slug: "collaborative-text-editor/nextjs-yjs-quill",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with a Quill text editor using Liveblocks and Svelte"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Quill text editor using Liveblocks and Svelte"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Svelte application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Quill</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs quill quill-cursors y-quill
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative Quill text editor</StepTitle>
    <StepContent>

      ```html file="Editor.svelte"
      <script>
      import { onMount, onDestroy } from "svelte";
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import Quill from "quill";
      import { QuillBinding } from "y-quill";
      import QuillCursors from "quill-cursors";

      let element;

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      onMount(() => {
        // Enter a multiplayer room
        const { room, leave } = client.enterRoom("my-room");

        // Set up Yjs document, shared text, and Liveblocks Yjs provider
        const yProvider = getYjsProviderForRoom(room);
        const yDoc = yProvider.getYDoc();
        const yText = yDoc.getText("quill");

        // Attach cursors plugin
        Quill.register("modules/cursors", QuillCursors);

        // Set up Quill editor and modules
        const quill = new Quill(element, {
          placeholder: "Start collaborating…",
          theme: "snow",
          modules: {
            cursors: true,
            toolbar: [
              [{ header: [1, 2, false] }],
              ["bold", "italic", "underline"],
              ["code-block"],
            ],
            history: {
              // Local undo shouldn’t undo changes made by other users
              userOnly: true,
            },
          },
        });

        // Attach Yjs to Quill
        const binding = new QuillBinding(yText, quill, yProvider.awareness);

        return () => {
          binding.destroy();
          leave();
        };
      });
      </script>

      <div bind:this={element}></div>
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative Quill
text editor inside your Svelte application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [Quill website](https://quilljs.com)

---
meta:
  title: "Get started with a Quill text editor using Liveblocks and Vue.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Quill text editor using Liveblocks and Vue.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Vue.js application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Quill</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs quill quill-cursors y-quill
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative Quill text editor</StepTitle>
    <StepContent>

      ```html file="Editor.vue"
      <script setup>
      import { ref, onMounted, onUnmounted } from "vue";
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import Quill from "quill";
      import { QuillBinding } from "y-quill";
      import QuillCursors from "quill-cursors";

      const element = ref(null);
      const binding = ref(null);
      const leave = ref(null);

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      // Enter a multiplayer room
      const info = client.enterRoom("my-room");
      const room = info.room;
      leave.value = info.leave;

      // Set up Yjs document, shared text, and Liveblocks Yjs provider
      const yProvider = getYjsProviderForRoom(room);
      const yDoc = yProvider.getYDoc();
      const yText = yDoc.getText("quill");

      // Attach cursors plugin
      Quill.register("modules/cursors", QuillCursors);

      onMounted(() => {
        // Set up Quill editor and modules
        const quill = new Quill(element.value, {
          placeholder: "Start collaborating…",
          theme: "snow",
          modules: {
            cursors: true,
            toolbar: [
              [{ header: [1, 2, false] }],
              ["bold", "italic", "underline"],
              ["code-block"],
            ],
            history: {
              // Local undo shouldn’t undo changes made by other users
              userOnly: true,
            },
          },
        });

        // Attach Yjs to Quill
        binding.value = new QuillBinding(yText, quill, yProvider.awareness);
      });

      onUnmounted(() => {
        binding?.destroy?.();
        leave?.();
      });
      </script>

      <template>
        <div ref="element" />
      </template>
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative Quill
text editor inside your Vue.js application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [Quill website](https://quilljs.com)

---
meta:
  title: "Get started with a Slate text editor using Liveblocks and React"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Slate text editor using Liveblocks and React"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your React application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Slate</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs slate slate-react @slate-yjs/core
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up the Liveblocks client</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people
      collaborate, and to create a realtime experience, multiple users must
      be connected to the same room. Set up a Liveblocks client with [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), and join a room with [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider).

      ```tsx file="App.tsx" highlight="11-15"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
      } from "@liveblocks/react/suspense";
      import { Editor } from "./Editor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              {/* ... */}
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      After setting up the room, you can add collaborative components inside it, using
      [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#ClientSideSuspense) to add loading spinners to your app.

      ```tsx file="App.tsx" highlight="14-16"
      "use client";

      import {
        LiveblocksProvider,
        RoomProvider,
        ClientSideSuspense,
      } from "@liveblocks/react/suspense";
      import { CollaborativeEditor } from "./CollaborativeEditor";

      export default function App() {
        return (
          <LiveblocksProvider publicApiKey={"{{PUBLIC_KEY}}"}>
            <RoomProvider id="my-room">
              <ClientSideSuspense fallback={<div>Loading…</div>}>
                <CollaborativeEditor />
              </ClientSideSuspense>
            </RoomProvider>
          </LiveblocksProvider>
        );
      }
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Set up the collaborative Slate text editor</StepTitle>
    <StepContent>

      Now that we set up Liveblocks, we can start integrating Slate and Yjs in the `CollaborativeEditor.tsx` file.
      To make the editor collaborative, we can rely on `withYjs` from `@slate-yjs/core`.

      ```tsx file="CollaborativeEditor.tsx"
      "use client";

      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import { useEffect, useMemo, useState } from "react";
      import { createEditor, Editor, Transforms } from "slate";
      import { Editable, Slate, withReact } from "slate-react";
      import { withYjs, YjsEditor } from "@slate-yjs/core";
      import * as Y from "yjs";
      import { useRoom } from "../liveblocks.config";
      import styles from "./CollaborativeEditor.module.css";

      export function CollaborativeEditor() {
        const room = useRoom();
        const [connected, setConnected] = useState(false);

        // Set up Yjs
        const yProvider = getYjsProviderForRoom(room);
        const yDoc = yProvider.getYDoc();
        const sharedType = yDoc.get("slate", Y.XmlText) as Y.XmlText;

        useEffect(() => {
          yProvider.on("sync", setConnected);

          return () => {
            yProvider?.off("sync", setConnected);
          };
        }, [room]);

        if (!connected || !sharedType) {
          return <div>Loading…</div>;
        }

        return <SlateEditor sharedType={sharedType} />;
      }

      const emptyNode = {
        children: [{ text: "" }],
      };

      function SlateEditor({ sharedType }: { sharedType: Y.XmlText }) {
        const editor = useMemo(() => {
          const e = withReact(withYjs(createEditor(), sharedType));

          // Ensure editor always has at least 1 valid child
          const { normalizeNode } = e;
          e.normalizeNode = (entry) => {
            const [node] = entry;

            if (!Editor.isEditor(node) || node.children.length > 0) {
              return normalizeNode(entry);
            }

            Transforms.insertNodes(editor, emptyNode, { at: [0] });
          };

          return e;
        }, []);

        useEffect(() => {
          YjsEditor.connect(editor);
          return () => YjsEditor.disconnect(editor);
        }, [editor]);

        return (
          <div className={styles.container}>
            <div className={styles.editorContainer}>
              <Slate editor={editor} initialValue={[emptyNode]}>
                <Editable className={styles.editor} placeholder="Start typing here…" />
              </Slate>
            </div>
          </div>
        );
      }
      ```

      And here is the `Editor.module.css` file to make sure your multiplayer text editor looks nice and tidy.

      ```css file="CollaborativeEditor.module.css" isCollapsed isCollapsable
      .container {
        display: flex;
        flex-direction: column;
        position: relative;
        border-radius: 12px;
        background: #fff;
        width: 100%;
        height: 100%;
        color: #111827;
      }

      .editor {
        border-radius: inherit;
        flex-grow: 1;
        width: 100%;
        height: 100%;
      }

      .editor:focus {
        outline: none;
      }

      .editorContainer {
        position: relative;
        padding: 1em;
        height: 100%;
      }

      .editor p {
        margin: 1em 0;
      }
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication
      endpoint. This approach is great for prototyping and marketing pages
      where defining your own security isn’t always required. If you want to
      implement your own security logic to define if certain users should
      have access to a given room, you’ll need to implement an
      authentication endpoint.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>

</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative Slate
text editor inside your React application.

- [Yjs and Slate guides](/docs/guides?technologies=yjs%2Cslate)
- [How to create a collaborative text editor with Slate, Yjs, Next.js, and Liveblocks](/docs/guides/how-to-create-a-collaborative-text-editor-with-slate-yjs-nextjs-and-liveblocks)
- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [Slate website](https://docs.slatejs.org/)

---

## Examples using Slate

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Text Editor",
      slug: "collaborative-text-editor/nextjs-yjs-slate",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Get started with a Tiptap text editor using Liveblocks and JavaScript"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Tiptap text editor using Liveblocks and JavaScript"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your JavaScript application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Tiptap</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor y-prosemirror
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Create an HTML element</StepTitle>
    <StepContent>

      ```html
      <div id="editor"></div>
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative Tiptap text editor</StepTitle>
    <StepContent>

      ```js file="app.js"
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import { Editor } from "@tiptap/core";
      import StarterKit from "@tiptap/starter-kit";
      import Collaboration from "@tiptap/extension-collaboration";
      import CollaborationCursor from "@tiptap/extension-collaboration-cursor";

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      // Enter a multiplayer room
      const { room, leave } = client.enterRoom("my-room");

      // Set up Yjs document and Liveblocks Yjs provider
      const yProvider = getYjsProviderForRoom(room);
      const yDoc = yProvider.getYDoc();

      // Set up the Tiptap editor
      const element = document.querySelector("#editor");
      const editor = new Editor({
        element,
        extensions: [
          StarterKit.configure({
            // The Collaboration extension comes with its own history handling
            undoRedo: false,
          }),
          // Register the Yjs document with Tiptap
          Collaboration.configure({
            document: yDoc,
          }),
          CollaborationCursor.configure({
            provider: yProvider,
          }),
        ],
      });
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Tiptap text editor inside your JavaScript application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [Tiptap website](https://tiptap.dev)

---
meta:
  title: "Get started with a Tiptap text editor using Liveblocks and Svelte"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Tiptap text editor using Liveblocks and Svelte"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Svelte application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Tiptap</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor y-prosemirror
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative Tiptap text editor</StepTitle>
    <StepContent>

      ```html file="Editor.svelte"
      <script>
      import { onMount, onDestroy } from "svelte";
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import { Editor } from "@tiptap/core";
      import StarterKit from "@tiptap/starter-kit";
      import Collaboration from "@tiptap/extension-collaboration";
      import CollaborationCursor from "@tiptap/extension-collaboration-cursor";

      let element;

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      onMount(() => {
        // Enter a multiplayer room
        const { room, leave } = client.enterRoom("my-room");

        // Set up Yjs document and Liveblocks Yjs provider
        const yProvider = getYjsProviderForRoom(room);
        const yDoc = yProvider.getYDoc();

        // Set up the Tiptap editor
        editor = new Editor({
          element,
          extensions: [
            StarterKit.configure({
              // The Collaboration extension comes with its own history handling
              undoRedo: false,
            }),
            // Register the Yjs document with Tiptap
            Collaboration.configure({
              document: yDoc,
            }),
            CollaborationCursor.configure({
              provider: yProvider,
            }),
          ],
          onTransaction: () => {
            // Force re-render so `editor.isActive` works as expected
            editor = editor
          },
        });

        return () => {
          editor.destroy();
          leave();
        };
      });
      </script>

      <div bind:this={element}></div>
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Tiptap text editor inside your Svelte application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [Tiptap website](https://tiptap.dev)

---
meta:
  title: "Get started with a Tiptap text editor using Liveblocks and Vue.js"
  parentTitle: "Quickstart"
  description:
    "Learn how to install a Tiptap text editor using Liveblocks and Vue.js"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start adding
collaboration to your Vue.js application using the APIs from the
[`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks, Yjs, and Tiptap</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

      ```bash trackEvent="install_liveblocks"
      npm install @liveblocks/client @liveblocks/yjs yjs @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor y-prosemirror
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-client#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework javascript
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up your collaborative Tiptap text editor</StepTitle>
    <StepContent>

      ```html file="Editor.vue"
      <script setup>
      import { onUnmounted } from "vue";
      import { createClient } from "@liveblocks/client";
      import { getYjsProviderForRoom } from "@liveblocks/yjs";
      import * as Y from "yjs";
      import { useEditor, EditorContent } from "@tiptap/vue-3";
      import StarterKit from "@tiptap/starter-kit";
      import Collaboration from "@tiptap/extension-collaboration";
      import CollaborationCursor from "@tiptap/extension-collaboration-cursor";

      const leave = ref(null);

      // Set up Liveblocks client
      const client = createClient({
        publicApiKey: "{{PUBLIC_KEY}}",
      });

      // Enter a multiplayer room
      const info = client.enterRoom("my-room");
      const room = info.room;
      leave.value = info.leave;

      // Set up Yjs document and Liveblocks Yjs provider
      const yProvider = getYjsProviderForRoom(room);
      const yDoc = yProvider.getYDoc();

      // Set up the Tiptap editor
      const editor = useEditor({
        element,
        extensions: [
          StarterKit.configure({
            // The Collaboration extension comes with its own history handling
            undoRedo: false,
          }),
          // Register the Yjs document with Tiptap
          Collaboration.configure({
            document: yDoc,
          }),
          CollaborationCursor.configure({
            provider: yProvider,
          }),
        ],
      });

      onUnmounted(() => {
        leave?.();
      });
      </script>

      <template>
        <editor-content :editor="editor" />
      </template>
      ```
    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation for your collaborative
Tiptap text editor inside your Vue.js application.

- [@liveblocks/yjs API Reference](/docs/api-reference/liveblocks-yjs)
- [@liveblocks/client API Reference](/docs/api-reference/liveblocks-client)
- [Tiptap website](https://tiptap.dev)

---
meta:
  title: "Get started with Liveblocks and Zustand"
  parentTitle: "Quickstart"
  description: "Learn how to get started with Liveblocks and Zustand"
---

Liveblocks is a realtime collaboration infrastructure for building performant
collaborative experiences. Follow the following steps to start making your
Zustand store multiplayer by using the middleware from the
[`@liveblocks/zustand`](/docs/api-reference/liveblocks-zustand) package.

## Quickstart

<Steps>
  <Step>
    <StepTitle>Install Liveblocks</StepTitle>
    <StepContent>

      Every Liveblocks package should use the same version.

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

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Initialize the `liveblocks.config.ts` file</StepTitle>
    <StepContent>

      We can use this file later to [define types for our application](/docs/api-reference/liveblocks-react#Typing-your-data).

      ```bash
      npx create-liveblocks-app@latest --init --framework react
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Connect your Zustand store to Liveblocks</StepTitle>
    <StepContent>

      Create the Liveblocks client and use the `middleware` in your
      Zustand store setup. This will add a new state called{" "}
      `liveblocks` to your store, enabling you to interact with
      our Presence and Storage APIs.

      ```ts file="store.ts" highlight="12-14,17-22"
      "use client";

      import create from "zustand";
      import { createClient } from "@liveblocks/client";
      import { liveblocks } from "@liveblocks/zustand";
      import type { WithLiveblocks } from "@liveblocks/zustand";

      type State = {
        // Your Zustand state type will be defined here
      };

      const client = createClient({
        publicApiKey: "pk_prod_xxxxxxxxxxxxxxxxxxxxxxxx",
      });

      const useStore = create<WithLiveblocks<State>>()(
        liveblocks(
          (set) => ({
            // Your state and actions will go here
          }),
          { client }
        )
      );

      export default useStore;
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Join a Liveblocks room</StepTitle>
    <StepContent>

      Liveblocks uses the concept of rooms, separate virtual spaces where people collaborate.
      To create a realtime experience, multiple users must be connected to the same room.

      ```tsx file="App.tsx" highlight="13,15"
      "use client";

      import React, { useEffect } from "react";
      import useStore from "./store";
      import "./App.css";

      const App = () => {
        const {
          liveblocks: { enterRoom, leaveRoom },
        } = useStore();

        useEffect(() => {
          enterRoom("room-id");
          return () => {
            leaveRoom("room-id");
          };
        }, [enterRoom, leaveRoom]);

        return <Room />;
      };

      export default App;
      ```

    </StepContent>

  </Step>
  <Step>
    <StepTitle>Use the Liveblocks data from the store</StepTitle>
    <StepContent>

      Now that we’re connected to a room, we can start using the Liveblocks data from the Zustand store.

      ```tsx file="Room.tsx" highlight="6"
      "use client";

      import useStore from "./store";

      export function Room() {
        const others = useStore((state) => state.liveblocks.others);
        const userCount = others.length;
        return <div>There are {userCount} other user(s) online</div>;
      }
      ```

    </StepContent>

  </Step>
  <Step lastStep>
    <StepTitle>Next: set up authentication</StepTitle>
    <StepContent>
      By default, Liveblocks is configured to work without an authentication endpoint
      where everyone automatically has access to rooms. This approach is great for
      prototyping and marketing pages where setting up your own security isn’t always
      required. If you want to limit access to a room for certain users, you’ll need
      to set up an authentication endpoint to enable permissions.

      <Button asChild className="not-markdown">
        <a href="/docs/authentication">
          Set up authentication
        </a>
      </Button>
    </StepContent>

  </Step>
</Steps>

## What to read next

Congratulations! You now have set up the foundation to start building
collaborative experiences for your Zustand store.

- [@liveblocks/zustand API Reference](/docs/api-reference/liveblocks-zustand)
- [Zustand guides](/docs/guides?technologies=zustand)
- [How to use Liveblocks Presence with Zustand](/docs/guides/how-to-use-liveblocks-presence-with-zustand)
- [How to use Liveblocks Storage with Zustand](/docs/guides/how-to-use-liveblocks-storage-with-zustand)

---

## Examples using Zustand

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative To-do List",
      slug: "collaborative-todo-list/zustand-todo-list",
      image: "/images/examples/thumbnails/collaborative-todo-list.jpg",
    }}
    technologies={["nextjs", "vuejs", "sveltekit", "solidjs", "javascript"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Whiteboard",
      slug: "collaborative-whiteboard/zustand-whiteboard",
      image: "/images/examples/thumbnails/collaborative-whiteboard.jpg",
      advanced: true,
    }}
    technologies={["nextjs", "redux", "zustand"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Flowchart",
      slug: "collaborative-flowchart/zustand-flowchart",
      image: "/images/examples/thumbnails/collaborative-flowchart.jpg",
      advanced: true,
    }}
    technologies={["zustand"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Documentation"
  description:
    "Explore the documentation to learn how to build collaborative experiences
    with Liveblocks."
showTitle: false
---

<DocsHomepageHero
  title="Documentation"
  description="Liveblocks gives you the building blocks and infrastructure to enable people and AI to work together inside your app."
  actions={[
    {
      title: "Get started",
      href: "/docs/get-started",
      appearance: "primary",
    },
    {
      title: "Browse examples",
      href: "/examples",
    },
  ]}
/>

<DocsHomepageGrid />

## Ready-made features

<ListGrid columns={2}>
  <DocsCardFeature
    feature="comments"
    href="/docs/ready-made-features/comments"
  />
  <DocsCardFeature
    feature="multiplayer"
    href="/docs/ready-made-features/multiplayer"
  />
  <DocsCardFeature
    feature="ai-agents"
    href="/docs/ready-made-features/ai-agents"
  />
  <DocsCardFeature
    feature="notifications"
    href="/docs/ready-made-features/notifications"
  />
</ListGrid>

## SDKs and packages

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="JavaScript"
    href="/docs/api-reference/liveblocks-client"
    description="@liveblocks/client"
    visual={<DocsJavascriptIcon />}
  />
  <DocsCard
    type="technology"
    title="React"
    href="/docs/api-reference/liveblocks-react"
    description="@liveblocks/react"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="React UI"
    href="/docs/api-reference/liveblocks-react-ui"
    description="@liveblocks/react-ui"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="React Tiptap"
    href="/docs/api-reference/liveblocks-react-tiptap"
    description="@liveblocks/react-tiptap"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="React BlockNote"
    href="/docs/api-reference/liveblocks-react-blocknote"
    description="@liveblocks/react-blocknote"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Node.js ProseMirror"
    href="/docs/api-reference/liveblocks-node-prosemirror"
    description="@liveblocks/node-prosemirror"
    visual={<DocsNodejsIcon />}
  />
  <DocsCard
    type="technology"
    title="React Lexical"
    href="/docs/api-reference/liveblocks-react-lexical"
    description="@liveblocks/react-lexical"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Node.js Lexical"
    href="/docs/api-reference/liveblocks-node-lexical"
    description="@liveblocks/node-lexical"
    visual={<DocsNodejsIcon />}
  />
  <DocsCard
    type="technology"
    title="Redux"
    href="/docs/api-reference/liveblocks-redux"
    description="@liveblocks/redux"
    visual={<DocsReduxIcon />}
  />
  <DocsCard
    type="technology"
    title="Zustand"
    href="/docs/api-reference/liveblocks-zustand"
    description="@liveblocks/zustand"
    visual={
      <img
        alt=""
        src="/assets/zustand.png"
        width={24}
        height={24}
        loading="lazy"
      />
    }
  />
  <DocsCard
    type="technology"
    title="Yjs"
    href="/docs/api-reference/liveblocks-yjs"
    description="@liveblocks/yjs"
    visual={<DocsYjsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Node.js"
    href="/docs/api-reference/liveblocks-node"
    description="@liveblocks/node"
    visual={<DocsNodejsIcon />}
  />
  <DocsCard
    type="technology"
    title="Node.js Emails"
    href="/docs/api-reference/liveblocks-emails"
    description="@liveblocks/emails"
    visual={<DocsNodejsIcon />}
  />
  <DocsCard
    type="technology"
    title="REST API"
    href="/docs/api-reference/rest-api-endpoints"
    description="HTTP endpoints"
    visual={<DocsApiIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Examples

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "AI Popup Chat",
      slug: "ai-popup/nextjs-ai-popup",
      image: "/images/examples/thumbnails/ai-popup.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "AI Chats",
      slug: "ai-chats/nextjs-ai-chats",
      image: "/images/examples/thumbnails/ai-chats.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Advanced Collaborative Text Editor",
      slug: "collaborative-text-editor-advanced/nextjs-tiptap-advanced",
      image: "/images/examples/thumbnails/text-editor-advanced.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Comments Notifications",
      slug: "comments-notifications",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Live Cursors",
      slug: "live-cursors",
      image: "/images/examples/thumbnails/live-cursors.jpg",
    }}
    technologies={["nextjs", "vuejs", "sveltekit", "solidjs", "javascript"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Whiteboard",
      slug: "collaborative-whiteboard-advanced",
      image:
        "/images/examples/thumbnails/collaborative-whiteboard-advanced.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

## Community

<ListGrid columns={3}>
  <DocsCard
    title="X"
    href="https://x.com/liveblocks"
    description="Follow us on X for the latest news and updates."
    visual={<DocsXIcon />}
    openInNewWindow
  />
  <DocsCard
    title="Discord"
    href="https://liveblocks.io/discord"
    description="Join hundreds of developers building with Liveblocks."
    visual={<DocsDiscordIcon />}
    openInNewWindow
  />
  <DocsCard
    title="GitHub"
    href="https://github.com/liveblocks/liveblocks"
    description="Follow progress and contribute to the codebase."
    visual={<DocsGithubIcon />}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Account management"
  parentTitle: "Platform"
  description: "Learn how to manage your Liveblocks account and team members."
---

In this section, you'll learn everything you need to manage your Liveblocks
accounts and teams.

<ListGrid columns={2}>
  <DocsCard
    title="Create an account"
    href="/docs/platform/account-management/create-an-account"
    description="Set up your Liveblocks account and get started"
  />
  <DocsCard
    title="Manage team members"
    href="/docs/platform/account-management/manage-team-members"
    description="Manage team members in your workspace"
  />
  <DocsCard
    title="Configure SSO"
    href="/docs/platform/account-management/saml"
    description="Set up SAML Single Sign-on for your organization"
  />
  <DocsCard
    title="Configure directory sync"
    href="/docs/platform/account-management/directory-sync"
    description="Sync users and groups from your directory service"
  />
  <DocsCard
    title="Delete an account"
    href="/docs/platform/account-management/delete-an-account"
    description="Permanently delete your Liveblocks account"
  />
</ListGrid>

---
meta:
  title: "Create an account"
  parentTitle: "Platform"
  description: "Learn how to create a Liveblocks account."
---

When you create a new account with Liveblocks, you’re automatically creating a
team account on a Starter plan. This plan is free to use (subject to the
[Fair Use Policy](/docs/platform/limits/fair-use-policy)) forever. You cannot
invite others to collaborate on your Liveblocks team account until you upgrade
to a paid plan.

To create a new account, visit [liveblocks.io/signup](/auth/signup). You can
choose to sign up with your email address, or with a GitHub or Google provider.

<Figure>
  <Image
    src="/assets/account-management/sign-up.png"
    alt="Create a Liveblocks account"
    width={768}
    height={406}
  />
</Figure>

### Sign up with email

If you choose to sign up with your email address, you’ll be prompted to enter it
in a form. After signing up, you can then verify your new account by clicking a
link in an email Liveblocks sends you. In future, when signing in with your
email, you’ll be asked to verify your account every time you log in.

### Sign up with a provider

If you choose to sign up with Google or GitHub provider, you will be asked to
authorize Liveblocks to access your provider account. This connection will then
become the default login connection on your account.

---
meta:
  title: "Delete an account"
  parentTitle: "Platform"
  description: "Learn how to delete your Liveblocks account and data."
---

<Banner type="warning">

Please note that this action is **irreversible**. All data associated with your
account will be permanently deleted, and any applications running on Liveblocks
will stop working.

</Banner>

To delete your account, click on the user dropdown in the Liveblocks dashboard
and go to **Personal settings**. Once there, click **Delete account** and follow
the on-screen instructions to confirm deletion.

<Figure>
  <Image
    src="/assets/account-management/delete-account.png"
    alt="Delete your Liveblocks account"
    width={768}
    height={406}
  />
</Figure>

## Getting help

If you need further assistance with account deletion or have questions about
what data will be removed, contact us at
[support@liveblocks.io](mailto:support@liveblocks.io).

---
meta:
  title: "Directory sync"
  parentTitle: "Platform"
  description: "Learn how to set up directory sync on your Liveblocks account."
---

<Banner>
  This feature is available as an add-on to Enterprise customers. Reach out to
  [sales@liveblocks.io](mailto:sales@liveblocks.io) to enable it on your
  account.
</Banner>

_Directory sync_ helps teams manage their organization membership from a
third-party identity provider like Google Directory or Okta. To use directory
sync, [SSO must be configured first](/docs/platform/account-management/saml) as
it requires an active SSO connection.

## How it works

When directory sync is configured, changes to your directory provider will
automatically be synced with your team members. The previously existing
permissions/roles will be overwritten by directory sync, including current user
performing the sync.

<Banner type="warning">

Make sure you’re a member of the groups from the directory provider that you’re
syncing with Liveblocks. Otherwise, you may lock yourself out.

</Banner>

You can configure a mapping between your directory provider’s groups and a
Liveblocks team role. For example, your _engineers_ group on Okta can be
configured with the _member_ role on Liveblocks, and your _admin_ group can use
the _owner_ role.

<Figure>
  <Image
    src="/assets/account-management/team-security-settings.png"
    alt="Liveblocks team security settings"
    width={768}
    height={406}
  />
</Figure>

### Configuring directory sync

1. To configure directory sync for your team, you must be an owner of the team
   (see
   [how to manage team members](/docs/platform/account-management/manage-team-members)).
2. From your dashboard, ensure your team is selected in the scope selector.
3. Navigate to the “Settings” tab and select “Security & Privacy”.
4. Navigate to the “Directory sync” section.
5. Click “Configure” and follow the walkthrough to configure directory sync for
   your team with your directory provider of choice.
6. Map your directory groups to appropriate Liveblocks team roles.

<Figure>
  <Image
    src="/assets/account-management/dsync-setup.png"
    alt="Liveblocks team security settings"
    width={768}
    height={406}
  />
</Figure>

---
meta:
  title: "Manage team members"
  parentTitle: "Platform"
  description: "Learn how to manage team members on your Liveblocks account."
---

Teams are made up of members, and each member can be assigned a role. These
roles define what you can and cannot do within a team on Liveblocks.

As your project scales, and you add more team members, you can assign each
member a role to ensure that they have the right permissions to work on your
projects.

## Inviting team members

To invite new members to your team, select the team from the scope selector,
then open the **Settings** tab and navigate to **Members**.

<Figure>
  <Image
    src="/assets/account-management/team-members.png"
    alt="Liveblocks team members"
    width={768}
    height={406}
  />
</Figure>

Enter the email address of the person you’d like to add, select their role, and
press the **Invite** button.

<Figure>
  <Image
    src="/assets/account-management/team-members-invite-dialog.png"
    alt="Invite team member to Liveblocks"
    width={768}
    height={406}
  />
</Figure>

## Access roles

Liveblocks offers two types of roles for team members, each providing different
levels of access and permissions.

### Owner role

As a team owner, you have full administrative control over your team. This
includes the ability to manage all aspects of account & project settings,
security, and billing.

Team owners can manage API keys in all projects and also change the roles of
other team members, including promoting members to owners. However, the only way
an owner can renounce their role is by either choosing to leave, or by deleting
the team altogether.

Teams can have more than one owner. For continuity, we recommend that at least
two individuals have owner permissions. Additional owners can be added without
any impact on existing ownership. Keep in mind that role changes, including
assignment and revocation of team member roles, are an exclusive capability of
those with the owner role.

### Member role

Those with the member role have the ability to create and manage projects. They
are also granted permissions to manage API keys for all projects.

However, there are certain team-level settings that are off-limits to members.
These include editing team settings, such as billing information, the ability
to invite new users to the team, and the ability to delete projects. This
restriction is in place to maintain the division of responsibilities and
control between members and owners.

---
meta:
  title: "Multi-factor authentication"
  parentTitle: "Platform"
  description:
    "Learn how to enable multi-factor authentication on your Liveblocks account."
---

<Banner>
  This feature is available as an add-on to Enterprise customers. Reach out to
  [sales@liveblocks.io](mailto:sales@liveblocks.io) to enable it on your
  account.
</Banner>

_Multi-factor authentication_ (MFA) adds an additional layer of security to
teams to manage their members by enforcing them to have an authenticator app.
Does not apply to Single Sign-On members.

## How it works

When MFA is enabled, all team members will be required to have an authenticator
app installed on their device. They will be prompted to enter a code from the
app when they log in to the dashboard.

<Figure>
  <Image
    src="/assets/account-management/team-mfa-setup.png"
    alt="Liveblocks team setup MFA"
    width={768}
    height={406}
  />
</Figure>

## How to enable MFA

MFA is available as an add-on to Enterprise customers, reach out to
[sales@liveblocks.io](mailto:sales@liveblocks.io) to enable it on your account.
After it’s enabled, it will work without configuration.

---
meta:
  title: "SAML Single Sign-on"
  parentTitle: "Platform"
  description: "Learn how to set up SSO on your Liveblocks account."
---

<Banner>
  This feature is available as a paid add-on on the Pro and Enterprise plans.
  Reach out to [sales@liveblocks.io](mailto:sales@liveblocks.io) to enable it on
  your account.
</Banner>

SSO enables teams to enforce authentication via identity providers like Okta,
Azure AD, Google Workspace, or OneLogin using SAML. This simplifies account
management, and helps organizations meet internal security requirements. Paired
with Directory Sync, teams can manage organization membership directly from
their identity provider, reducing manual overhead and aligning with standard
enterprise access control practices.

Once enabled, all team members will be able to log in to the dashboard using
your selected identity provider. You’ll still need to invite users to your team
for them to have access. If you would like all users signing up with SSO to be
automatically added to your team, please contact us.

You can also use
[Directory Sync](/docs/platform/account-management/directory-sync) to
automatically assign users to your team with a specific role.

<Figure>
  <Image
    src="/assets/account-management/team-security-settings.png"
    alt="Liveblocks team security settings"
    width={768}
    height={406}
  />
</Figure>

## Configuring SSO

After SSO is enabled on your account, you can configure it for your team.

1. To configure SSO, you must be an owner of the team (see
   [how to manage team members](/docs/platform/account-management/manage-team-members)).
2. From your dashboard, ensure your team is selected in the scope selector.
3. Navigate to the “Settings” tab and select “Security & Privacy”.
4. Navigate to the “SSO” section.
5. Click “Configure” and follow the walkthrough to configure SSO for your team
   with the identity provider of your choice.

<Figure>
  <Image
    src="/assets/account-management/sso-setup.png"
    alt="Liveblocks team security settings"
    width={768}
    height={406}
  />
</Figure>

## Enforcing SSO

For additional security, SSO can be enforced for a team so that all team members
cannot access any team information unless their current session is authenticated
with SSO. If you want to activate this setting, please contact us at
[support@liveblocks.io](mailto:support@liveblocks.io).

### Automatic provisioning

By default, you still need to manually invite users to your team for them to
have access to it. If you would like all users signing up with SSO to be
automatically added to your team, please contact us at
[support@liveblocks.io](mailto:support@liveblocks.io).

## Authenticating with SSO

Once you have configured SSO, your team members can use it to log in or sign up
to Liveblocks. They only have to enter their email, and they will be redirected
to the third-party authentication provider that you configured.

## Supported providers

Liveblocks supports the following third-party identity providers:

- Okta
- Entra ID (Azure AD)
- Google SAML
- ADP OpenID Connect
- Auth0 SAML
- CAS SAML
- ClassLink SAML
- Cloudflare SAML
- CyberArk SAML
- Duo SAML
- Entra ID OpenID Connect
- Google OpenID Connect
- JumpCloud SAML
- Keycloak SAML
- LastPass SAML
- Microsoft AD FS
- NetIQ SAML
- Okta OpenID Connect
- OneLogin
- Oracle
- PingFederate
- PingOne
- Rippling
- Salesforce
- SimpleSAMLphp SAML
- VMware Workspace One

If your identity provider is not listed or if you'd like to use an OIDC (OpenID
Connect) SSO provider, please reach out to us at
[support@liveblocks.io](mailto:support@liveblocks.io).

## Domain verification

Liveblocks supports self-serve domain verification for SSO. This allows you to
verify domains and sign in through your organization’s SSO connection without
needing to verify your team members’ emails.

After SSO is enabled on your account you can verify domains from the dashboard.

1. From your dashboard, ensure your team is selected in the scope selector.
2. Navigate to the “Settings” tab and select “Security & Privacy”.
3. Navigate to the “SSO” section.
4. Click "Add domain” and follow the walkthrough to verify domains for your
   team.

<Figure>
  <Image
    src="/assets/account-management/sso-domain-verification.png"
    alt="Liveblocks team security settings"
    width={768}
    height={406}
  />
</Figure>

---
meta:
  title: "Analytics"
  parentTitle: "Platform"
  description:
    "With analytics, you’re able to gather insights into collaborative
    experiences for a given project with metrics like daily active users,
    monthly active users, connections, active rooms, and more."
---

With analytics, you’re able to gather insights into collaborative experiences
for a given project with metrics such as daily active users, monthly active
users, connections, active rooms, and more.

<Figure>
  <Image
    src="/assets/analytics/analytics.jpg"
    alt="Liveblocks analytics"
    width={768}
    height={406}
  />
</Figure>

You can adjust the period through the timeframe selector by picking between 1
hour, 1 day, 1 week, and 1 month.

<Figure>
  <Image
    src="/assets/analytics/analytics-timeframe.jpg"
    alt="Liveblocks analytics timeframe selector"
    width={768}
    height={406}
  />
</Figure>

---
meta:
  title: "Data storage"
  parentTitle: "Platform"
  description:
    "Learn about Liveblocks data storage, retention policies, and deletion
    procedures."
---

Liveblocks is designed to support realtime collaboration and AI features with a
focus on performance, reliability, and privacy. While many aspects of data
handling are managed by the platform, enterprise customers can configure certain
behaviors related to data storage, retention, and region enforcement. This
document outlines what data Liveblocks stores, where it is stored, and how
deletion is handled.

<Banner title="Trust center">

Visit our [trust center](https://liveblocks.safebase.us/) to download security
and compliance reports, such as SOC 2 Type II and HIPAA.

</Banner>

## Data stored

In most cases, sensitive or personal information can be excluded from what's
sent to Liveblocks. Additionally, expanded storage configuration and data
control features are under active development for enterprise customers.

### Comments

- **Data stored**: Comment body, author ID, timestamps, mentions, tags, and
  metadata.
- **Retention**: Persists until the room or thread is explicitly deleted.
  Additionally, if all comments in a thread are removed, the thread is deleted.

### Multiplayer

#### Realtime data storage

- **Data stored**: Collaborative state written in text editor and sync engine
  integrations. This includes Tiptap, BlockNote, Lexical, Storage (e.g.
  `LiveObject`), and Yjs (e.g. `Y.Doc`).
- **Retention**: Persists until the room is explicitly deleted. Storage (e.g.
  `LiveObject`) data can be deleted without deleting the room.

#### Presence

- **Data stored**: Room ID, user ID, and approximate geolocation (based on IP
  address). This is used to display session events in the Liveblocks Dashboard.
- **Retention**: Persists until a deletion request is submitted.

### AI Agents

#### Liveblocks AI Copilots

- **Data stored**: Prompt configuration, LLM connection parameters (including
  API keys), message history, tool results, token usage, model usage, chat
  names, and metadata. Back-end knowledge is an optional add-on that requires
  saving text and vectors to support knowledge recall.
- **Retention**: Persists until the user or client deletes a chat.

### Notifications

- **Data stored**: Notification message, delivery state, and recipient user ID.
- **Retention**: Persists until each notification is explicitly deleted.

### Webhooks

- **Data stored**:
  [Webhook event data](/docs/platform/webhooks#Liveblocks-events), such as
  project ID, room ID, and the event type.
- **Retention**: Automatically deleted after 90 days.

## Storage locations

### AWS

AWS is used to store various data with `us-east-1` being used as the default
region. Enterprise customers can choose to
[region lock data](/docs/platform/projects#Project-location) in `us-east-1` or
`eu-central-1`, per project.

#### Stored in AWS

- Room metadata (ID, accesses, metadata).
- Comments.
- Notifications.
- Project usage analytics.
- Dashboard members.
- User events.
- AI Copilot configuration.
- AI Copilot uploaded/crawled knowledge.

#### Encryption

Postgres and MongoDB data is encrypted at rest, including backups, replicas, and
snapshots.

### Cloudflare

Realtime collaboration data used in Multiplayer and Presence is handled by
Cloudflare’s global edge network. This data is handled on the edge, as close to
the user as possible, to ensure low-latency performance. Because Cloudflare
manages routing dynamically across its global edge network, data processed in
WebSocket sessions is not guaranteed to remain within a specific geographic
region, apart from when region-locking is enabled.

Enterprise customers can choose to
[region lock data](/docs/platform/projects#Project-location) to `Cloudflare EU`
or `Cloudflare FedRAMP`, per project.

#### Stored in Cloudflare

- Multiplayer data for text editor and sync engine integrations.
- AI Copilot message, tool, chat, and usage history.

#### Encryption

DurableObject data is encrypted at rest.

### Svix

Svix is used to deliver webhook messages.

#### Stored in Svix

- Webhook message data.

## Data deletion

### Comments

Comment data persists until the room is deleted with
[`deleteRoom`](/docs/api-reference/liveblocks-node#delete-rooms-roomId) or
[Delete Room REST API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId).
Additionally, individual threads and their comments can be deleted explicitly
with [`useDeleteThread`](/docs/api-reference/liveblocks-react#useDeleteThread),
[`deleteThread`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-threads-threadId)
or
[Delete Thread REST API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-threads-threadId).

Comments can also be deleted by their authors—when a single comment is deleted,
its body is cleared and marked with a `deletedAt` timestamp. When all comments
in a thread are deleted, the thread is deleted too.

### Multiplayer

#### Realtime data storage

Multiplayer data is deleted when the associated room is removed with
[`deleteRoom`](/docs/api-reference/liveblocks-node#delete-rooms-roomId) or
[Delete Room REST API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId).
Additionally, Storage data can be explicitly deleted with
[`deleteStorageDocument`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-storage)
or
[Delete Storage Document REST API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-storage).

#### Presence

Presence data persists until a deletion request is submitted.

### AI Agents

#### Liveblocks AI Copilots

Chat data is deleted by the user or client calling
[`useDeleteAiChat`](/docs/api-reference/liveblocks-react#useDeleteAiChat).

### Notifications

Notifications are persisted until explicitly deleted with
[`deleteInboxNotification`](/docs/api-reference/liveblocks-node#delete-users-userId-inboxNotifications-inboxNotificationId)
or
[Delete Inbox Notification REST API](/docs/api-reference/rest-api-endpoints#delete-users-userId-inboxNotifications-inboxNotificationId).

### Webhooks

Webhook messages data is automatically deleted after 90 days.

## Data security

To learn more about data security, visit our trust center to find detailed
information and downloadable compliance reports, including SOC 2 Type II and
HIPAA.

<Button asChild className="not-markdown">
  <a href="https://liveblocks.safebase.us/">Open trust center</a>
</Button>

---
meta:
  title: "Limits"
  parentTitle: "Platform"
  description:
    "A list of all the limits and limitations that apply on Liveblocks."
---

## General limits

There are three plans: Free, Pro, Team, and Enterprise. Each plan has its own
set of limits.

|                                                                                                                                                                                                                                                                                                        | Free                                              | Pro                                                                                                                                                   | Team                                                                                                                                                    | Enterprise                                                                                                                                            |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| <a href="#What-is-a-monthly-active-room">Monthly active rooms</a>                                                                                                                                                                                                                                      | <Limits.FreeMaxMonthlyActiveRooms />              | <Limits.ProMonthlyActiveRoomsIncluded /> included<br /><span className="text-xs text-product-subtlest"><Limits.ProMonthlyActiveRoomsOverage /></span> | <Limits.TeamMonthlyActiveRoomsIncluded /> included<br /><span className="text-xs text-product-subtlest"><Limits.TeamMonthlyActiveRoomsOverage /></span> | <Limits.EnterpriseMonthlyActiveRoomsIncluded /> <InfoTooltip>Up to <Limits.EnterpriseMaxMonthlyActiveRooms /></InfoTooltip>                           |
| <a href="#What-is-a-monthly-active-user">Monthly active users</a>                                                                                                                                                                                                                                      | <Limits.FreeMaxMau />                             | <Limits.ProMaxMau />                                                                                                                                  | <Limits.TeamMaxMau />                                                                                                                                   | <Limits.EnterpriseMaxMau />                                                                                                                           |
| <a href="/docs/platform/projects">Projects</a>                                                                                                                                                                                                                                                         | <Limits.FreeMaxProjects />                        | <Limits.ProMaxProjects />                                                                                                                             | <Limits.TeamMaxProjects />                                                                                                                              | <Limits.EnterpriseProjectsIncluded /> <InfoTooltip>Up to <Limits.EnterpriseMaxProjects /></InfoTooltip>                                               |
| <a href="/docs/platform/account-management/manage-team-members">Team members</a>                                                                                                                                                                                                                       | <Limits.FreeMaxTeamMembersPerAccount />           | <Limits.ProTeamMembersPerAccount /> included<br /><span className="text-xs text-product-subtlest"><Limits.ProTeamMembersPerAccountOverage /></span>   | <Limits.TeamTeamMembersPerAccount /> included<br /><span className="text-xs text-product-subtlest"><Limits.TeamTeamMembersPerAccountOverage /></span>   | <Limits.EnterpriseTeamMembersPerAccount /> <InfoTooltip>Up to <Limits.EnterpriseMaxTeamMembersPerAccount /></InfoTooltip>                             |
| <a href="#What's-a-simultaneous-connection-per-room" title="Learn more">Simultaneous connections per room</a>                                                                                                                                                                                          | <Limits.FreeMaxConnectionsPerRoom />              | <Limits.ProMaxConnectionsPerRoom />                                                                                                                   | <Limits.TeamMaxConnectionsPerRoom />                                                                                                                    | <Limits.EnterpriseMaxConnectionsPerRoom />                                                                                                            |
| <a href="#What's-a-simultaneous-connection-per-project">Simultaneous connections per project</a>                                                                                                                                                                                                       | <Limits.FreeMaxConnectionsPerProject />           | <Limits.ProMaxConnectionsPerProject />                                                                                                                | <Limits.TeamMaxConnectionsPerProject />                                                                                                                 | <Limits.EnterpriseMaxConnectionsPerProject />                                                                                                         |
| Monthly anonymous connections <InfoTooltip>Public key only recommended for prototyping. <a href="/docs/authentication" className="font-medium">Set up authentication</a>.</InfoTooltip>                                                                                                                | <Limits.FreeMaxAnonymousConnections />            | <Limits.ProMaxAnonymousConnections />                                                                                                                 | <Limits.TeamMaxAnonymousConnections />                                                                                                                  | <Limits.EnterpriseMaxAnonymousConnections />                                                                                                          |
| Realtime data storage <InfoTooltip>This is a cumulative calculation that represents all data that is currently stored. It does not reset monthly, and the total usage will continue to accrue over time as you add more content and data.</InfoTooltip>                                                | <Limits.FreeMaxRealtimeDataStorage />             | <Limits.ProRealtimeDataStorage /> included<br /><span className="text-xs text-product-subtlest"><Limits.ProRealtimeDataStorageOverage /></span>       | <Limits.TeamRealtimeDataStorage /> included<br /><span className="text-xs text-product-subtlest"><Limits.TeamRealtimeDataStorageOverage /></span>       | <Limits.EnterpriseRealtimeDataStorage /> <InfoTooltip>Up to <Limits.EnterpriseMaxRealtimeDataStorage /></InfoTooltip>                                 |
| Realtime data stored per room                                                                                                                                                                                                                                                                          | <Limits.FreeMaxStorageSizePerRoom />              | <Limits.ProMaxStorageSizePerRoom />                                                                                                                   | <Limits.TeamMaxStorageSizePerRoom />                                                                                                                    | <Limits.EnterpriseMaxStorageSizePerRoom />                                                                                                            |
| File storage <InfoTooltip>This is a cumulative calculation that represents all files that are stored with Liveblocks. It does not reset monthly, and the total usage will continue to accrue over time as you add more files. Files can be uploaded as comment attachments for instance.</InfoTooltip> | <Limits.FreeMaxFileStorage />                     | <Limits.ProFileStorage /> included<br /><span className="text-xs text-product-subtlest"><Limits.ProFileStorageOverage /></span>                       | <Limits.TeamFileStorage /> included<br /><span className="text-xs text-product-subtlest"><Limits.TeamFileStorageOverage /></span>                       | <Limits.EnterpriseMaxFileStorage />                                                                                                                   |
| Max file upload size                                                                                                                                                                                                                                                                                   | <Limits.FreeMaxFileUploadSize />                  | <Limits.ProMaxFileUploadSize />                                                                                                                       | <Limits.TeamMaxFileUploadSize />                                                                                                                        | <Limits.EnterpriseFileUploadSize /> <InfoTooltip>Up to <Limits.EnterpriseMaxFileUploadSize /></InfoTooltip>                                           |
| Comments stored                                                                                                                                                                                                                                                                                        | <Limits.FreeMaxComments />                        | <Limits.ProMaxComments />                                                                                                                             | <Limits.TeamMaxComments />                                                                                                                              | <Limits.EnterpriseMaxComments />                                                                                                                      |
| Monthly collaboration notifications <InfoTooltip>Notifications automatically triggered from collaborative features like comment thread updates and @ mentions.</InfoTooltip>                                                                                                                           | <Limits.FreeMaxCollaborationNotifications />      | <Limits.ProMaxCollaborationNotifications />                                                                                                           | <Limits.TeamMaxCollaborationNotifications />                                                                                                            | <Limits.EnterpriseMaxCollaborationNotifications />                                                                                                    |
| Monthly custom notifications <InfoTooltip>Non-collaborative notifications that can be triggered from your own code.</InfoTooltip>                                                                                                                                                                      | <Limits.FreeMaxCustomNotifications />             | <Limits.ProCustomNotifications /> included<br /><span className="text-xs text-product-subtlest"><Limits.ProCustomNotificationsOverage /></span>       | <Limits.TeamCustomNotifications /> included<br /><span className="text-xs text-product-subtlest"><Limits.TeamCustomNotificationsOverage /></span>       | <Limits.EnterpriseCustomNotifications /> <InfoTooltip>Up to <Limits.EnterpriseMaxCustomNotifications /></InfoTooltip>                                 |
| Webhook event frequency <InfoTooltip>The frequency at which `yDocUpdated` (Yjs) and `storageUpdated` (Liveblocks Storage) events are sent to your webhook endpoint. <a href="/docs/platform/webhooks#StorageUpdatedEvent" className="font-medium">Learn more</a></InfoTooltip>                         | <Limits.FreeStorageUpdatedWebhookEventThrottle /> | <Limits.ProStorageUpdatedWebhookEventThrottle />                                                                                                      | <Limits.TeamStorageUpdatedWebhookEventThrottle />                                                                                                       | <Limits.EnterpriseStorageUpdatedWebhookEventThrottle /> <InfoTooltip>Down to <Limits.EnterpriseMaxStorageUpdatedWebhookEventThrottle /></InfoTooltip> |
| Version history <InfoTooltip>APIs, hooks, and pre-built components to access the previous versions of the realtime data stored in rooms.</InfoTooltip>                                                                                                                                                 | <Limits.FreeVersionHistory />                     | <Limits.ProVersionHistory /> <InfoTooltip>Unlimited with Version History Plus add‑on</InfoTooltip>                                                    | <Limits.TeamVersionHistory /> <InfoTooltip>Unlimited with Version History Plus add‑on</InfoTooltip>                                                     | <Limits.EnterpriseVersionHistory />                                                                                                                   |
| Event log retention                                                                                                                                                                                                                                                                                    | <Limits.FreeEventLogRetention />                  | <Limits.ProEventLogRetention />                                                                                                                       | <Limits.TeamEventLogRetention />                                                                                                                        | <Limits.EnterpriseEventLogRetention /> <InfoTooltip>Up to <Limits.EnterpriseMaxEventLogRetention /></InfoTooltip>                                     |

## Other limits

| Item                                                                                                                                                                                                  | Limit                                                              |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `roomId` <InfoTooltip>The unique identifier for a room. <a href="#What's-a-room" className="font-medium">What's a room?</a></InfoTooltip>                                                             | 128 characters                                                     |
| `userId` <InfoTooltip>The unique identifier for a user. Each `userId` must be used to represent a single user.</InfoTooltip>                                                                          | 128 characters                                                     |
| `userInfo` <InfoTooltip>User information sent from the authentication endpoint.</InfoTooltip>                                                                                                         | 1024 characters once serialized to JSON                            |
| Broadcast event message <InfoTooltip>Messages sent via the broadcast API for real-time communication between clients.</InfoTooltip>                                                                   | 1 MB                                                               |
| Thread metadata <InfoTooltip>Custom metadata attached to a thread.</InfoTooltip>                                                                                                                      | 50 properties, 4000 characters each                                |
| Comment metadata <InfoTooltip>Custom metadata attached to a comment.</InfoTooltip>                                                                                                                    | 50 properties, 4000 characters each                                |
| `LiveObject` <InfoTooltip>A realtime data structure that stores key-value pairs. <a href="/docs/api-reference/liveblocks-client#LiveObject" className="font-medium">Learn more</a></InfoTooltip>      | 128 kB when totalling the size of the keys and values              |
| `LiveMap` <InfoTooltip>A realtime data structure that stores key-value pairs in a map. <a href="/docs/api-reference/liveblocks-client#LiveMap" className="font-medium">Learn more</a></InfoTooltip>   | Unlimited, so long as each individual value does not exceed 128 kB |
| `LiveList` <InfoTooltip>A realtime data structure that stores an ordered list of items. <a href="/docs/api-reference/liveblocks-client#LiveList" className="font-medium">Learn more</a></InfoTooltip> | Unlimited, so long as each individual value does not exceed 128 kB |

Note that when one realtime data structure is nested inside another, it does not
count towards the limit. Only the JSON leaves of your data structure count
towards the limit. For example, if a `LiveList` is nested inside a `LiveObject`,
the `LiveList` and its contents do not count towards the `LiveObject`'s data
limit.

## FAQs

### What is a monthly active room?

Monthly active rooms represents the number of rooms that have been used within a
given month. Rooms are identified by their `roomId`.

Specifically, a room counts as active when any of these occur:

- A user connects on the front-end.
- A comment is added or modified.
- A document is updated. Any of the following: Storage, Tiptap, BlockNote,
  Lexical, Yjs.
- An attachment is uploaded.

If a room contains content from the previous month, it is not active until it is
used in the current month. For example, if a text document is created in
January, it is not active in February until a user connects to the document or
updates its content.

### What is a monthly active user?

Monthly active users represents the number of users that have used any
Liveblocks features within a given month. Users are identified by the `userId`
property provided in your [authentication endpoint](/docs/authentication). This
`userId` generally comes from your database or your authentication provider.

One unique `userId` corresponds to one user, no matter how long they connect
for. For example, a user that connects for 5 minutes on 2 different days is
counted as 1 MAU, and a user that connects 8 hours a day for a month is also
counted as 1 MAU. This applies so long as usage falls under our
[Fair Use Policy](/docs/platform/limits/fair-use-policy).

### What’s a connection?

A connection occurs when a user opens a web page containing a Liveblocks room.
Each tab counts as one connection, which means that a user with two open tabs
has two connections. A room is joined when using{" "}
[`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider) in React or
[`enterRoom`](/docs/api-reference/liveblocks-client#Client.enterRoom) in
JavaScript. Liveblocks connections are analogous to WebSocket connections,
representing a realtime link to our server.

### What’s a room?

A room is the virtual space where people collaborate. For most products, a room
corresponds to a document.

### What’s a simultaneous connection per room?

A simultaneous connection per room is when a user or multiple users are
connected to the same Liveblocks room at the same time. Learn more about
handling simultaneous room connection limits in our guide about
[joining rooms at maximum capacity](/docs/guides/what-happens-when-a-user-joins-a-room-at-maximum-capacity).

### What’s a simultaneous connection per project?

A simultaneous connection per project is when a user or multiple users are
connected to the same project at the same time.

### How is realtime data storage calculated?

Realtime data storage is a cumulative calculation that represents all realtime
data that is currently stored in your rooms. It does not reset monthly, and the
total usage will continue to accrue over time as you add more content and data.
You can monitor your usage at any time in the [dashboard](/dashboard).

### What happens when limits are reached?

Liveblocks returns a different error for each limit that a user might reach.
That way, you can decide how to best handle those cases in your product.

### Do you count monthly active rooms during testing?

Yes, if test users connect to a room during automated testing, the room will be
counted as a monthly active room. To prevet hitting limits during testing, you
may want to provide a set of reusable `roomId`s.

---
meta:
  title: "Fair use guidelines"
  parentTitle: "Limits"
  description: "Learn about Liveblocks’ fair use guidelines."
---

All subscription plans include usage that is subject to these fair use
guidelines. These guidelines define acceptable usage and how we evaluate usage
patterns over time to ensure stable performance for all customers.

## Examples of fair use

<StyledList>
  <StyledListItem type="check">A collaborative SaaS product</StyledListItem>
  <StyledListItem type="check">An AI chatbot</StyledListItem>
  <StyledListItem type="check">A multiplayer game</StyledListItem>
  <StyledListItem type="check">
    An interactive multiplayer landing page
  </StyledListItem>
</StyledList>

## Never fair use

<StyledList>
  <StyledListItem type="cross">
    Using a single `userId` for multiple users
  </StyledListItem>
  <StyledListItem type="cross">Abusing free REST API endpoints</StyledListItem>
  <StyledListItem type="cross">
    Load testing without authorization
  </StyledListItem>
  <StyledListItem type="cross">
    Not using Liveblocks for collaboration
  </StyledListItem>
  <StyledListItem type="cross">
    Sharing logins or using group accounts for the admin dashboard
  </StyledListItem>
</StyledList>

## Usage guidelines

As a guideline for our community, we expect most projects to fall within the
ranges outlined below for each plan. If your usage significantly exceeds these
guidelines, we’ll contact you to discuss your needs and determine whether
changes to your plan or implementation are required. We aim to accommodate
legitimate usage while ensuring our platform remains stable and performant for
all users.

If your usage patterns are abnormal and materially impact platform performance,
as determined at Liveblocks’ discretion based on platform-wide considerations,
we may require changes to your implementation or a plan change to support the
load. We will notify you if changes are required to your account before taking
any action that impacts your account.

### Typical monthly usage guidelines

|                                   | Free                                         | Pro and Team                                |
| --------------------------------- | -------------------------------------------- | ------------------------------------------- |
| Average connections per month     | Up to <Limits.FreeAvgConnectionsPerMau />    | Up to <Limits.ProAvgConnectionsPerMau />    |
| Total active room hours per month | Up to <Limits.FreeActiveRoomHoursPerMonth /> | Up to <Limits.ProActiveRoomHoursPerMonth /> |

Usage is measured at the team workspace level. For teams on the Pro and Team
plans, you can pay for [additional usage](/docs/platform/limits) as you go.

### Additional usage

For members of our Pro and Team plans, we offer a pay-as-you-go model for
additional usage, giving you greater flexibility and control over your usage.
The typical monthly usage guidelines above are still applicable, while extra
usage will be automatically charged at the following rates:

| Item                                                                             | Included                                 | Pay-as-you-go                              |
| -------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------ |
| <a href="#What-is-a-monthly-active-room">Monthly active rooms</a>                | <Limits.ProMonthlyActiveRoomsIncluded /> | <Limits.ProMonthlyActiveRoomsOverage />    |
| <a href="/docs/platform/account-management/manage-team-members">Team members</a> | <Limits.ProTeamMembersPerAccount />      | <Limits.ProTeamMembersPerAccountOverage /> |
| Realtime data storage                                                            | <Limits.ProRealtimeDataStorage />        | <Limits.ProRealtimeDataStorageOverage />   |
| File storage                                                                     | <Limits.ProFileStorage />                | <Limits.ProFileStorageOverage />           |
| Monthly custom notifications                                                     | <Limits.ProCustomNotifications />        | <Limits.ProCustomNotificationsOverage />   |

Usage beyond included amounts will continue to be billed at pay-as-you-go rates
unless another arrangement is put in place.

### Commercial usage

The Free plan is limited to non-commercial, personal projects only. Any
commercial use of Liveblocks requires a Pro, Team or Enterprise subscription.

We consider usage to be commercial when a project generates revenue or financial
benefit for anyone involved in its development, operation, or maintenance. This
includes projects built by paid employees, contractors, or consultants. Internal
use within a company qualifies as commercial usage, regardless of whether users
pay directly. Common examples of commercial usage include:

- Applications that accept payments or process transactions
- Products or services offered for sale
- Projects developed or maintained for compensation
- Sites primarily focused on affiliate marketing
- Applications displaying advertisements or sponsored content

If you’re uncertain whether your project qualifies as commercial usage, please
reach out to our support team for clarification.

### General Limits

Take a look at our [Limits](/docs/platform/limits) documentation for the limits
we apply to all accounts.

### Learn more

Circumventing or attempting to bypass Liveblocks’ limits or usage guidelines is
a violation of our fair use guidelines. For further information regarding these
guidelines and acceptable use of our services, refer to our
[Terms of Service](/terms) or your Enterprise Service Agreement. In case of
conflict, contractual terms supersede these guidelines. These guidelines may be
updated over time as Liveblocks releases new features or changes platform
capacity.

---
meta:
  title: "Projects"
  parentTitle: "Platform"
  description:
    "To use Liveblocks, you need to create a project, a place to group your
    collaborative rooms. Learn how to set up and configure projects in this
    guide."
---

To use Liveblocks, you need to create a project, a place to group your
collaborative rooms. Learn how to set up and configure projects in this guide.

## Creating a project

You can create a new project from the [Liveblocks dashboard](/dashboard) by
clicking on the **Create project…** button.

<Figure>
  <Image
    src="/assets/projects/create-project.jpg"
    alt="Start creating a project from the Liveblocks dashboard"
    width={768}
    height={209}
  />
</Figure>

### Project environment

A project’s environment can either be set to **Development** or **Production**,
helping you map projects to your deployment model. We recommend setting up a new
project for each different environment your collaborative application uses.

<Figure>
  <Image
    src="/assets/projects/create-project-dialog.jpg"
    alt="Creating a project from the Liveblocks dashboard"
    width={768}
    height={499}
  />
</Figure>

Secret [API keys](#project-api-keys) are treated slightly differently depending
on the environment:

- **Development** environments allow you to read your secret key any time after
  generation. It’s recommended to use this setting in development, preview, and
  staging environments.
- **Production** environments encrypt your secret key, meaning it can only be
  read when it’s first generated. If you forget your secret key, you must roll a
  new one. It’s recommended to use this environment in your production
  application.

<Banner>

A project’s environment cannot be changed later.

</Banner>

### Project location

When creating a new project in the dashboard, teams on the
[Enterprise plan](https://liveblocks.io/pricing) can specify which region the
project data will be stored and processed. There are three options:

1. N/A (No preference)
2. EU (AWS eu-central-1, Cloudflare EU)
3. US (AWS us-east-1, Cloudflare FedRAMP)

<Banner>

A project’s location cannot be changed later.

</Banner>

## Managing a project

Each Liveblocks project has a separate dashboard to monitor usage, configure
settings, manage API keys, and more.

### Project overview

The **Overview** tab displays an overview of your project usage, providing
information on active rooms, users, and connections.

<Figure>
  <Image
    src="/assets/projects/project-overview.jpg"
    alt="Liveblocks project overview"
    width={768}
    height={499}
  />
</Figure>

### Project rooms

The **Rooms** tab displays all the rooms in your project. Use the search bar
next to the page title to find rooms. By default, rooms are sorted by the last
connection date (most recently active rooms). You can also sort them by room ID,
number of threads, document size, or creation date by clicking on the column
labels.

<Figure>
  <Image
    src="/assets/projects/project-rooms.jpg"
    alt="Liveblocks project rooms"
    width={768}
    height={499}
  />
</Figure>

You can learn more about any given room by clicking on it. On the room detail
view, you’re able to view the stored document’s data and common actions you can
take on it, such as deleting the document’s data.

<Figure>
  <Image
    src="/assets/projects/project-rooms-detail.jpg"
    alt="Liveblocks project rooms detail"
    width={768}
    height={499}
  />
</Figure>

### Project API keys [#project-api-keys]

The **API keys** tab enables you to manage, view, and roll your **public** and
**secret** API keys.

<Figure>
  <Image
    src="/assets/projects/project-api-keys.jpg"
    alt="Liveblocks project API keys"
    width={768}
    height={499}
  />
</Figure>

### Project webhooks

The **Webhooks** tab enables you to configure your webhook endpoints allowing
you to respond to Liveblocks events, such as a user entering a room, or storage
being updated. See our [webhooks](/docs/platform/webhooks) docs to learn more.

### Project settings

The **Settings** tab enables you to rename your project. Team owners can also
delete projects from this tab.

---
meta:
  title: "Liveblocks REST API"
  parentTitle: "Platform"
  description:
    "Liveblocks REST API allows developers to interact programmatically with
    their Liveblocks account and services using HTTP requests. With the API,
    developers can retrieve, set, and update room-related data, permissions,
    schemas, and more."
---

Liveblocks REST API allows developers to interact programmatically with their
Liveblocks account and services using HTTP requests. With the API, developers
can retrieve, set, and update room-related data, users, permissions, schemas,
and more.

The Liveblocks API is organized around REST. The API has predictable
resource-oriented URLs, accepts form-encoded request bodies, returns
JSON-encoded responses, and uses standard HTTP response codes, authentication,
and verbs. See the [API reference](api-reference/rest-api-endpoints) for more
information.

```
https://api.liveblocks.io/v2
```

---
meta:
  title: "Webhooks"
  parentTitle: "Platform"
  description: "Learn Liveblocks webhooks concepts and implementation."
---

Webhooks enable developers to extend the Liveblocks platform. From your system,
you can listen to events that get automatically triggered as users interact with
collaborative rooms.

## Configuring webhooks

To set up webhooks for your project, you’ll need to create an endpoint,
subscribe to events, and secure your endpoint.

- [Creating an endpoint](#creating-an-endpoint)
- [Edit endpoint events](#edit-endpoint-events)
- [Security verification](#security-verification)
- [Replaying events](#replaying-events)
- [Testing locally](#testing-locally)

### Creating an endpoint [#creating-an-endpoint]

If you would like to create an endpoint to receive webhook events, you will do
so from within the webhooks dashboard for your project.

<Steps>
  <StepCompact>

    From the <a href="/dashboard" target="_blank">dashboard</a> overview, navigate
    to the project you’d like to add webhooks to.

  </StepCompact>

  <StepCompact>

    Click on the webhooks tab from the left-hand menu.

  </StepCompact>

  <StepCompact>

    Click the **“Create endpoint…”** button.

  </StepCompact>

  <StepCompact>

    Enter the URL of the endpoint you would like to use. Configure with your own
    endpoint or generate a
    <a href="https://www.svix.com/play/" target="_blank">Svix playground</a> link
    by clicking on **"use Svix play"**.

  </StepCompact>

  <StepCompact>

    Select the events you would like to subscribe to.

  </StepCompact>

  <StepCompact lastStep>

    Click **“Create endpoint”**.

  </StepCompact>

</Steps>

<Figure>
  <video width={1512} height={982} autoPlay loop muted playsInline>
    <source src="/assets/webhooks/create-endpoint.mp4" type="video/mp4" />
  </video>
</Figure>

Your endpoint must return a `2xx` (status code `200-299`) to indicate that the
event was successfully received. If your endpoint returns anything else, the
event will be retried, see [replaying events](#replaying-events) for more
details.

If all events fail to be delivered to your endpoint for 5 consecutive days, your
endpoint will automatically be disabled. You can always re-enable it from the
dashboard.

### Edit endpoint events [#edit-endpoint-events]

You can easily edit the events you want to subscribe to after creating an
endpoint.

<Steps>
  <StepCompact>

    Select the endpoint you would like to edit from the list of webhooks in the
    dashboard.

  </StepCompact>

  <StepCompact>

    Select **“Edit endpoint…”** from the top right dropdown.

  </StepCompact>

  <StepCompact lastStep>

    Update event selections and click **“Save changes”**.

  </StepCompact>

</Steps>

<Figure>
  <video width={1512} height={982} autoPlay loop muted playsInline>
    <source src="/assets/webhooks/edit-events.mp4" type="video/mp4" />
  </video>
</Figure>

### Replaying events [#replaying-events]

If your service is unreachable, message retries are automatically re-attempted.
If your service incurs considerable downtime (over 8 hours), you can replay
individual messages from the Endpoints portion of the dashboard by clicking the
kebab menu on an individual message, or you can opt to bulk replay events by
clicking the top right dropdown and selecting **“Recover failed messages…”**.

<Figure>
  <video autoPlay loop muted playsInline>
    <source src="/assets/webhooks/replay-events.mp4" type="video/mp4" />
  </video>
</Figure>

Each message is attempted based on a schedule that follows the failure of the
preceding attempt. If an endpoint is removed or disabled, delivery attempts will
also be disabled. The schedule for retries is as follows:

- Immediately
- 5 seconds
- 5 minutes
- 30 minutes
- 2 hours
- 5 hours
- 10 hours
- 10 hours (in addition to the previous)

For example, an attempt that fails three times before eventually succeeding will
be delivered roughly 35 minutes and 5 seconds following the first attempt.

## Security verification [#security-verification]

Verifying webhooks prevents security vulnerabilities by safeguarding against
man-in-the-middle, CSRF, and replay attacks. Because of this, it is essential to
prioritize verification in your integration. We recommend using the
`@liveblocks/node` package to verify and return fully typed events.

<Steps>

  <Step>
    <StepTitle>Install the package</StepTitle>
    <StepContent>

      ```bash
      npm install @liveblocks/node
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Set up the webhook handler</StepTitle>
    <StepContent>

      Set up your webhook handler, inserting your secret key from the webhooks dashboard you
      set up earlier into [`WebhookHandler`](/docs/api-reference/liveblocks-node#WebhookHandler).

      ```ts
      import { WebhookHandler } from "@liveblocks/node";

      // Insert your webhook secret key
      const webhookHandler = new WebhookHandler("whsec_...");
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Verify an event request</StepTitle>
    <StepContent>

      We can verify a genuine webhook request with
      [`WebhookHandler.verifyRequest`](/docs/api-reference/liveblocks-node#verifyRequest)

      ```ts
      const event = webhookHandler.verifyRequest({
        headers: req.headers,
        rawBody: req.body,
      });
      ```

      <Banner title="rawBody takes a string" type="warning">

        Note that some frameworks parse request bodies into objects, so you may need to
        use `rawBody: JSON.stringify(req.body)` instead.

      </Banner>

      The method will return a `WebhookEvent` object that is fully typed. You can then
      use the event to perform actions based on the event type. If the request is not valid, an error will be thrown.

    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>

      Full example

    </StepTitle>

    <StepContent>

      Here’s an example from start to finish.

      ```ts
      import { WebhookHandler } from "@liveblocks/node";

      // Will fail if not properly initialized with a secret
      const webhookHandler = new WebhookHandler("whsec_...");

      export default function webhookRequestHandler(req, res) {
        try {
          const event = webhookHandler.verifyRequest({
            headers: req.headers,
            rawBody: req.body,
          });

          // Use the event, for example...
          if (event.type === "storageUpdated") {
            // { roomId: "my-room-name", projectId: "8sfhs5s...", ... }
            console.log(event.data);
          }
        } catch (error) {
          console.error(error);
          return res.status(400).end();
        }

        res.status(200).end();
      }
      ```

    </StepContent>

  </Step>

</Steps>

### Manually verify in Node.js

It’s also possible to manually verify your webhooks in Node.js, though it’s
unlikely this’ll be necessary.

<details>

<summary>How to manually verify webhook events in Node.js</summary>

<Steps>

  <Step>
    <StepTitle>Construct the signed content</StepTitle>
    <StepContent>

      The content to sign is composed by concatenating the request’s id, timestamp,
      and payload, separated by the full-stop character (`.`). In code, it will look
      something like:

      ```ts
      const crypto = require("crypto");

      // webhookId comes from the `webhook-id` header
      // webhookTimestamp comes from the `webhook-timestamp` header
      // body is the request body
      signedContent = `${webhookId}.${webhookTimestamp}.${body}`;
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Generate the signature</StepTitle>
    <StepContent>

      Liveblocks uses an HMAC with SHA-256 to sign its webhooks.

      So to calculate the expected signature, you should HMAC the `signedContent` from
      above using the base64 portion of your webhook secret key (this is the part
      after the `whsec_` prefix) as the key. For example, given the secret
      `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw` you will want to use
      `MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw`.

      For example, this is how you can calculate the signature in Node.js:

      ```ts
      // Your endpoint’s secret key
      const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";

      // Need to base64 decode the secret
      const secretBytes = new Buffer(secret.split("_")[1], "base64");
      // This is the signature you will compare against the signature header
      const signature = crypto
      .createHmac("sha256", secretBytes)
      .update(signedContent)
      .digest("base64");
      ```

    </StepContent>

  </Step>

  <Step>
    <StepTitle>Validate the signature</StepTitle>
    <StepContent>

      The generated signature should match one of the signatures sent in the
      `webhook-signature` header.

      The `webhook-signature` header comprises a list of space-delimited signatures
      and their corresponding version identifiers. The signature list is most commonly
      of length one. Though there could be any number of signatures. For example:

      ```
      v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=
      ```

      Make sure to remove the version prefix and delimiter (e.g., `v1`) before
      verifying the signature.

    </StepContent>

  </Step>

  <Step lastStep>
    <StepTitle>Verify the timestamp</StepTitle>
    <StepContent>

      As mentioned above, Liveblocks also sends the timestamp of the attempt in the
      `webhook-timestamp` header. You should compare this timestamp against your
      system timestamp and make sure it’s within your tolerance to prevent timestamp
      attacks.

      <Banner title="Comparing signatures">

        We recommend implementing a constant-time string comparison method when
        comparing signatures to prevent timing attacks.

      </Banner>

    </StepContent>

  </Step>

</Steps>

</details>

### Manually verify in Elixir

It’s also possible to manually verify your webhooks in Elixir using
Plug/Phoenix, especially if you want to validate Liveblocks webhooks before
parsing the request body.

<details>

<summary>How to manually verify webhook events in Elixir</summary>

<Steps>

<Step>
<StepTitle>Construct the signed content</StepTitle>
<StepContent>

The signed content is composed by concatenating the webhook ID, timestamp, and
request body, separated by dots (`.`). In Elixir, it looks like this:

```elixir
signed_content = "#{webhook_id}.#{webhook_timestamp}.#{body}"
```

- `webhook_id` comes from the `"webhook-id"` header.
- `webhook_timestamp` comes from the `"webhook-timestamp"` header.
- `body` is the raw request body.

</StepContent>
</Step>

<Step>
<StepTitle>Generate the signature</StepTitle>
<StepContent>

Liveblocks signs webhooks using HMAC with SHA-256. You need to use the
base64-decoded portion of your secret (after the `whsec_` prefix) as the key.

Example in Elixir:

```elixir
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"