--- 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: "", // "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", }); ``` Returns a [Client](#Client), used for connecting to Liveblocks. 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). 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). 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). When set, navigating away from the current page is prevented while Liveblocks is still synchronizing local changes. [Learn more](#prevent-users-losing-unsaved-changes). 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). The time before an inactive WebSocket connection is disconnected. This is disabled by default, but setting a number will activate it. [Learn more](#createClientBackgroundKeepAliveTimeout). 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). 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). 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). 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). 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).
How to handle WebSocket messages that are larger than the maximum message size. Can be set to one of these values:
- `"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.
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). Experimental. Stream the initial Storage content over HTTP, instead of waiting for a large initial WebSocket message to be sent from the server.
### 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: "", "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. Removing the "Powered by Liveblocks" badge on your projects requires a [paid plan](/pricing/). See the [pricing page](/pricing/) for more information. ## 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). A [Room](#Room), used for building your Liveblocks application. Learn more about [typing your room](#typing-your-data). A function that’s used to leave the room and disconnect. The ID of the room you’re connecting to. 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). 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). Whether the room immediately connects to Liveblocks servers. #### 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). 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). The ID of the room you’re connecting to. ### Client.getSyncStatus Gets the current Liveblocks synchronization status. ```ts const syncStatus = client.getSyncStatus(); // "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. ### 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(); ``` _Nothing_ _None_ #### 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 }) => ( {!result.data ? (
Looking up user...
) : (
Found user: {result.data.user.name}
)}
), }); ``` 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+. An AI tool. A clear description of what the tool does. Used by AI to understand when to call this tool. JSON Schema defining the tool’s input parameters. The AI will validate arguments against this schema. 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. 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). React component function that renders the tool’s UI during different execution stages. See [tool call rendering stages](#tool-call-rendering-stages). 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
Found user: {result.data.user.name}
; } // Tool hasn’t executed yet return ; }, }); ``` 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 (
); }, }); ``` 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 ( {stage === "receiving" && (

Preparing flight booking...

{partialArgs.origin &&

From: {partialArgs.origin}

} {partialArgs.destination &&

To: {partialArgs.destination}

} {partialArgs.departureDate && (

Date: {partialArgs.departureDate}

)} {partialArgs.passengers && (

Passengers ({partialArgs.passengers.length}):

    {partialArgs.passengers.map((passenger, index) => (
  • {passenger?.name || "Loading..."} {passenger?.age && ` (${passenger.age})`}
  • ))}
)}
)} {stage === "executing" && (

Booking flight...

{args.origin} → {args.destination} on {args.departureDate}

{args.passengers.length} passenger(s)

)} {stage === "executed" && result.data && (

Flight booked successfully!

Booking ID: {result.data.bookingId}

)}
); }, }); ``` 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 }; }; } } ``` 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). _None_ ### 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(); ``` _Nothing_ 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. Adds Presence values to the history stack, meaning using undo and redo functions will change them. [Learn more](#add-presence-to-history). #### 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 } ``` 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. _None_ ### 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: "🔥" }); ``` _Nothing_ 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). 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). #### 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. We’re not sure if we want to support `shouldQueueEventIfNotReady` in the future, so it may be deprecated and replaced with something else. ```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(); ``` Returns the current [`User`](#user-type). Returns `null` if the client is not yet connected to the room. _None_ 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); ```
Returns the room’s current connection status. It can return one of five values:
- `"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.
_None_ ### 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); ```
The current room’s Storage status. `status` can be one of four types.
- `"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.
_None_ ### 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 }); ``` Unsubscribe function. Call it to cancel the subscription. The `LiveObject`, `LiveMap`, or `LiveList` which is being subscribed to. Each time the structure is updated, the callback is called. Function that’s called when `storageItem` updates. Returns the updated storage structure. 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). #### 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; }; } } ``` ```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 } }); // +++ ``` Unsubscribe function. Call it to cancel the subscription. Listen to events. 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) #### 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 }); ``` Unsubscribe function. Call it to cancel the subscription. Listen to the current user’s presence. Function that’s called when the current user’s Presence has updated, for example with [`Room.updatePresence`][]. Receives the updates Presence value. #### 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 }); ``` Unsubscribe function. Call it to cancel the subscription. Listen to others. 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). #### 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); }); ``` Unsubscribe function. Call it to cancel the subscription. Listen to status updates. void`} required >
Function that’s called when the room’s connection status has changed. It can return one of five values:
- `"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.
#### 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); }); ``` Unsubscribe function. Call it to cancel the subscription. Listen to lost connection events. void`} required >
Function that’s called when a room’s lost connection event has been triggered. It can return one of three values:
- `"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.
#### 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; } }); ``` Unsubscribe function. Call it to cancel the subscription. Listen to error events. void`} required >
Function that’s called when an unrecoverable error event has been triggered. `error.code` can return one of these values:
- `-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.
#### 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 }); ``` Unsubscribe function. Call it to cancel the subscription. Listen to history events. 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). ### 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; } }); ``` Unsubscribe function. Call it to cancel the subscription. Listen to Storage status events. void`} required >
Function that’s called when the current user’s Storage updated status have changed. `status` can be one of four types.
- `"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
### 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 } }); }); ``` Returns the return value from the callback. A callback containing every Storage and Presence notification that will be part of the batch. Cannot be an `async` function. #### 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; ``` Note that to undo or redo in Yjs, you must use a separate history manager, [`Y.UndoManager`](https://docs.yjs.dev/api/undo-manager). #### 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"); ``` _Nothing_ _None_ ### 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"); ``` _Nothing_ _None_ ### 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(); ``` Whether there is an undo operation in the current history stack. _None_ ### 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(); ``` Whether there is a redo operation in the current history stack. _None_ ### 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(); ``` _Nothing_ _None_ ### 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"); ``` _Nothing_ _None_ ### 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"); ``` _Nothing_ _None_ ### 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(); ``` _Nothing_ _None_ ### Room.reconnect Reconnect the local room instance to the Liveblocks server, using a new WebSocket connection. ```ts room.reconnect(); ``` _Nothing_ _None_ ### 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(); ``` _Nothing_ _None_ ## 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); ``` Threads within the current room. Inbox notifications associated with the threads. Subscriptions associated with the threads. The request date to use for subsequent polling. Only return `resolved` or `unresolved` threads. [Learn more](#filtering-resolved-status). Only return `subscribed` or `unsubscribed` threads. [Learn more](#filtering-subscribed-status). Only return threads containing the custom metadata. Metadata is set yourself when creating a thread, for example `{ priority: "HIGH" }`. [Learn more](#filtering-metadata). #### 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); ``` Threads that have been updated or deleted since the requested date. Inbox notifications that have been updated or deleted since the requested date. Subscriptions that have been updated or deleted since the requested date. The request date to use for subsequent polling. Only return threads that have been updated or deleted after this date. ### 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); ``` The requested thread, or `undefined` if it doesn’t exist. The inbox notification associated with the thread, or `undefined` if it doesn’t exist. The subscription associated with the thread, or `undefined` if it doesn’t exist. The ID of the thread you want to retrieve. ### 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" }] }], }, }); ``` The thread that has been created. The content of the comment, see [creating thread content](#creating-thread-content). The IDs of the comment’s attachments. Custom metadata to be attached to the thread, see [defining thread metadata](#defining-thread-metadata). #### 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** > >
> _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. > **@Jody Hekla** 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 import { ThreadMetadata } from "@liveblocks/client"; const metadata: 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"); ``` _Nothing_ The ID of the thread to delete. ### 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, }, }); ``` The thread metadata. The ID of the thread. 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`. ### Room.markThreadAsResolved Marks a thread as resolved. ```ts await room.markThreadAsResolved("th_xxx"); ``` _Nothing_ The ID of the thread to resolve. ### Room.markThreadAsUnresolved Marks a thread as unresolved. ```ts await room.markThreadAsUnresolved("th_xxx"); ``` _Nothing_ The ID of the thread to resolve. ### 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"); ``` The thread’s subscription. The ID of the thread to subscribe to. #### 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"); ``` _Nothing_ The ID of the thread to unsubscribe from. #### 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" }] }], }, }); ``` The comment that has been created. The ID of the thread that the comment will be added to. The IDs of the comment’s attachments. The content of the comment, see [creating comment content](#creating-comment-content). #### 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** > >
> _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. > **@Jody Hekla** 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!" }, ], }, // +++ ], }; ``` ### Room.editComment Edits a comment, replacing its existing comment body and optionally updating its attachments. 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" }] }], }, }); ``` The comment that has been edited. The ID of the thread containing the comment. The ID of the comment that’s being edited. The IDs of the comment’s attachments. The content of the comment, see [creating comment content](#creating-comment-content). ### 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", }); ``` _Nothing_ The ID of the thread containing the comment. The ID of the comment that’s being edited. ### 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: "👍", }); ``` The reaction that has been created. The ID of the thread containing the comment. The ID of the comment to add a reaction to. The emoji reaction to add. ### Room.removeReaction Removes a reaction from a comment. ```ts await room.removeReaction({ threadId: "th_xxx", commentId: "cm_xxx", emoji: "👍", }); ``` _Nothing_ The ID of the thread containing the comment. The ID of the comment to remove a reaction from. The emoji reaction to remove. ### 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); ``` The local attachment that has been created. The file to create the attachment from. ### 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) }); ``` The attachment that has been uploaded. The file to create the attachment from. A set of options. Only the inbox notifications updated or deleted after this date will be returned. ### Room.getAttachmentUrl Returns a presigned URL for an attachment by its ID. ```ts const url = await room.getAttachmentUrl("at_xxx"); // "https://..." console.log(url); ``` A presigned URL for the attachment. The ID of the attachment to get the URL for. ## 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); ``` Current user’s inbox notifications. Threads associated with the inbox notifications. Subscriptions associated with the inbox notifications. The request date to use for subsequent polling. Only return inbox notifications for the given room. [Learn more](#filtering-inbox-notifications). Only return inbox notifications for the kind. [Learn more](#filtering-inbox-notifications). #### 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); ``` Inbox notifications that have been updated or deleted since the requested date. Threads that have been updated or deleted since the requested date. The request date to use for subsequent polling. Only the inbox notifications updated or deleted after this date will be returned. ### Client.getUnreadInboxNotificationsCount Gets the number of unread inbox notifications for the current user. ```ts const count = await client.getUnreadInboxNotificationsCount(); ``` Number of unread inbox notifications. _None_ ### Client.markAllInboxNotificationsAsRead Marks all inbox notifications as read, for the current user. ```ts await client.markAllInboxNotificationsAsRead(); ``` _Nothing_ _None_ ### Client.markInboxNotificationAsRead Marks an inbox notification as read, for the current user. ```ts await client.markInboxNotificationAsRead("in_xxx"); ``` _Nothing_ The ID of the inbox notification to be marked as read. ### Client.deleteAllInboxNotifications Deletes an inbox notification for the current user. ```ts await client.deleteAllInboxNotifications(); ``` _Nothing_ _None_ ### Client.deleteInboxNotification Deletes an inbox notification for the current user. ```ts await client.deleteInboxNotification("in_xxx"); ``` _Nothing_ The ID of the inbox notification to be deleted. ### 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(); ``` Subscription settings for Liveblocks products.
Returns the current room’s subscription settings for threads. It can return one of three 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.
Returns the current room’s subscription settings for text mentions. It can be one of two values:
- `"mine"` Receive notifications for mentions of you. - `"none"` No notifications are received.
_None_ ### 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", }); ``` Subscription settings for Liveblocks products.
Returns the current room’s subscription settings for threads. It can return one of three 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.
Returns the current room’s subscription settings for text mentions. It can be one of two values:
- `"mine"` Receive notifications for mentions of you. - `"none"` No notifications are received.
Sets the current room’s subscription settings for threads. It can be one of three 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.
Sets the current room’s subscription settings for text mentions. It can be one of two values:
- `"mine"` Receive notifications for mentions of you. - `"none"` No notifications are received.
#### 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); ``` Current user’s notification settings. _None_ ### 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); ``` Current user’s notification settings. A deep partial object containing the notification settings to update. Custom notifications can be set too.
```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, }, }); ```
The email notification settings. The Slack notification settings. The Microsoft Teams notification settings. The Web Push notification settings.
## 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; }>; declare global { interface Liveblocks { Storage: { people: LiveMap; }; } } ``` ```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); ``` Get the [Liveblocks DevTools extension](/devtools) to develop and debug your application as you build it. ### 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(); ``` The room’s Storage structures. `root` is a `LiveObject`, and is the root of your Storage. Learn more about [typing Storage](#typing-storage). _None_ ## 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" }); ``` The newly created `LiveObject`. The initial value for the `LiveObject`. Can contain JSON-serializable data and other Liveblocks conflict-free data structures. #### 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(); ``` _Nothing_ The key of the property you’re deleting. If the property doesn’t exist, nothing will occur. ### get [#LiveObject.get] Get a property from the `LiveObject`. ```ts const object = new LiveObject({ firstName: "Ada", lastName: "Lovelace" }); // "Ada" object.get("firstName"); ``` The value of the property. Returns `undefined` if it doesn’t exist. The key of the property you’re getting. ### 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(); ``` _Nothing_ The key of the property you’re setting. The value of the property you’re setting. Can contain JSON-serializable data and other Liveblocks conflict-free data structures. ### 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(); ``` _Nothing_ 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. ### 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()); ``` The cloned `LiveObject`. _None_ ### 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(); ``` 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. _None_ ### toObject [#LiveObject.toObject] Starting with 0.18, we recommend [`toImmutable`][] instead. It’s faster, cached, and leads to fewer surprises. 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: } // ❗️ ``` ## 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>; ``` 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"], ]); ``` The newly created `LiveMap`. 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. #### 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(); ``` If the element existed and was removed. The key of the element you’re deleting. If the element doesn’t exist, nothing will occur. ### 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 } ``` If your TypeScript project targets es5 or lower, you’ll need to enable the --downlevelIteration option to use this API. A new Iterator object for the `LiveMap`, containing the `[key, value]` pairs for each element. _None_ ### 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)); ``` _Nothing_ A callback for each entry. The callback is passed the current `value`, `key`, and the `LiveMap`. Return values are ignored. ### 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"); ``` The value of the entry. Returns `undefined` if it doesn’t exist. The key of the entry you’re getting. ### 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"); ``` Whether the entry exists. The key of the entry you’re getting. ### 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 } ``` If your TypeScript project targets es5 or lower, you’ll need to enable the --downlevelIteration option to use this API. A new Iterator object for the `LiveMap`, containing the keys of each entry. _None_ ### 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(); ``` _Nothing_ The key of the entry you’re setting. The value of the entry you’re setting. Can contain JSON-serializable data and other Liveblocks conflict-free data structures. ### size [#LiveMap.size] Returns the number of elements in the `LiveMap`. ```ts const map = new LiveMap([ ["nimesh", "developer"], ["pierre", "designer"], ]); // 2 map.size; ``` The number of entries in the `LiveMap` _N/A_ ### 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 } ``` If your TypeScript project targets es5 or lower, you’ll need to enable the --downlevelIteration option to use this API. A new Iterator object for the `LiveMap`, containing the values of each entry. _None_ ### 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()); ``` The cloned `LiveMap`. _None_ ### 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(); ``` 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. _None_ ## 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; ``` 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"]); ``` The newly created `LiveList`. The initial array of values for the `LiveList`. Can contain JSON-serializable data and other Liveblocks conflict-free data structures. ### clear [#LiveList.clear] Removes all the elements. ```ts const list = new LiveList(["adrien", "jonathan"]); list.clear(); // [] list.toImmutable(); ``` _Nothing_ _None_ ### 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"]`. _Nothing_ The index of the property you’re deleting. If the property doesn’t exist, an `Error` is thrown. ### 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); ``` Whether all elements pass the test implemented by the provided function. 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`. ### 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); ``` An array containing each item of the `LiveList` that passed the test implemented by the provided function. 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`. ### 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")); ``` The item that has been found. If no item passes the test, `undefined` is returned. 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`. ### 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")); ``` The index of the item that has been found. If no item passes the test, `-1` is returned. 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`. ### 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)); ``` _Nothing_ A callback for each item. The callback is passed the current `value` and `index`. Return values are ignored. ### 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); ``` The value of the item at the index. Returns `undefined` if it doesn’t exist. The index of the item you’re getting. ### 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"); ``` The index of the item. Returns `-1` if it doesn’t exist. The item you’re locating. The index to start the search at. ### 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(); ``` _Nothing_ The value of the item you’re inserting. The index to insert the item into. ### 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"); ``` The index of the item. Returns `-1` if it doesn’t exist. The item you’re locating. The index at which to start searching backwards. ### length [#LiveList.length] Returns the number of elements. ```ts const list = new LiveList(["adrien", "jonathan"]); // 2 list.length; ``` The number of items in the `LiveList`. _N/A_ ### 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()); ``` The array of each item has been transformed by the callback function. A callback for each item. The callback is passed the current `value` and `index`. Return values are used in the returned array. ### 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(); ``` _Nothing_ The index of the item to move. The index where the element should be after moving. ### 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(); ``` _Nothing_ The item to add to the end of the `LiveList`. ### 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")); ``` Whether any elements pass the test implemented by the provided function. 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`. ### 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()); ``` The cloned `LiveList`. _None_ ### 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(); ``` 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. _None_ ### toArray [#LiveList.toArray] Starting with 0.18, we recommend [`toImmutable`][] instead. It’s faster, cached, and leads to fewer surprises. 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(); // [ ] // ❗️ ``` ## 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); ``` 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). ### 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 }) => `

${children}

`, // Optional, override the `text` element text: ({ element }) => element.bold ? `${element.text}` : `${element.text}`, // Optional, override the `link` element link: ({ element, href }) => `${element.url}`, // Optional, override the `mention` element. // `user` and `group` are the optional data returned from `resolveUsers` and `resolveGroupsInfo` mention: ({ element, user, group }) => `${ element.id }`, }, // 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, })); }, }); ``` 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). #### 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" }]; }, }); // "Hello @Marc from // https://liveblocks.io" await stringifyCommentBody(comment.body, { format: "html", resolveUsers() { return [{ name: "Marc" }]; }, }); // "Hello @Marc from // https://liveblocks.io" await stringifyCommentBody(comment.body, { format: "html", mention: ({ element, user }) => `${user.name}`, 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 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; }; 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 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; }; // 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( "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 = { description: string; parameters: JSONSchema7; execute: ( args: TArgs, context: ToolExecutionContext ) => Promise<{ data: TResult }>; render: (props: ToolRenderProps) => ReactNode; }; ``` A clear description of what the tool does. Used by AI to understand when to call this tool. JSON Schema defining the tool’s input parameters. The AI will validate arguments against this schema. Async function that performs the tool’s action. Receives validated arguments and execution context, returns structured data. React component function that renders the tool’s UI during different execution stages. ### 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; }; ``` A clear description of what this knowledge represents (e.g., "Current user’s profile", "Application settings"). The knowledge content. Can be any JSON-compatible format that provides context to the AI. 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(); ``` The connection ID of the User. It is unique and increments with every new connection. The ID of the User that has been set in the authentication endpoint. Useful to get additional information about the connected user. Additional user information that has been set in the authentication endpoint. The user’s Presence data. `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). `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). [`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 = (
@{emailData.comment.author.id} at {emailData.comment.createdAt}
{emailData.comment.body}
); // 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).
An email showing 7 new comments, with comment bodies and links to each comment
This screenshot shows a ready-made template from our [Comments + Resend](/examples/comments-emails/nextjs-comments-emails-resend) example. 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 = (
@{emailData.comment.author.id} at {emailData.comment.createdAt}
{emailData.comment.body}
); break; } case "unreadReplies": { email = (
{emailData.comments.map((comment) => (
@{comment.author.id} at {comment.createdAt}
{comment.body}
))}
); 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 = (
@{emailData.comment.author.id} at {emailData.comment.createdAt}
{emailData.comment.body}
); break; } case "unreadReplies": { email = (
{emailData.comments.map((comment) => (
@{comment.author.id} at {comment.createdAt}
{comment.body}
))}
); break; } } // Send your email // ... } // +++ return new Response(null, { status: 200 }); } ``` 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 , // The formatted comment, pass it to React `children` body: { /* ... */}, author: { id: "aurélien@example.com", info: { /* Custom user info you have resolved */ }, }, }, } ```
```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 , // The formatted comment, pass it to React `children` body: { /* ... */}, author: { id: "aurélien@example.com", info: { /* Custom user info you have resolved */ }, }, }, // More comments //... ], } ```
A [`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client) Node.js client. An object passed from a webhook event, specifically the [`ThreadNotificationEvent`](/docs/platform/webhooks#Thread-notification). [Learn more about setting this up](#Setup). A number of options to customize the format of the comments, adding user info, room info, and styles. 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). 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). 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). 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). The comment body container. The paragraph block. The text element. The link element. ReactNode`} > The mention element. #### 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. 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. ```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 }) =>

{children}

, // `react-email` components are supported Text: ({ children }) => ( {children} ), // `user` and `group` are the optional data returned from `resolveUsers` and `resolveGroupsInfo` Mention: ({ element, user, group }) => ( @{user?.name ?? group?.name ?? element.id} ), // If the link is rich-text render it, otherwise use the URL Link: ({ element, href }) => {element?.text ?? href}, }, // +++ } ); // { 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 = `
@${emailData.comment.author.id} at ${emailData.comment.createdAt}
${emailData.comment.body}
`; break; } case "unreadReplies": { email = `
${emailData.comments .map( (comment) => `
@${comment.author.id} at ${comment.createdAt}
${comment.body}
` ) .join("")}
`; 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 = `
@${emailData.comment.author.id} at ${emailData.comment.createdAt}
${emailData.comment.body}
`; break; } case "unreadReplies": { email = `
${emailData.comments .map( (comment) => `
@${comment.author.id} at ${comment.createdAt}
${comment.body}
` ) .join("")}
`; break; } } // Send your email // ... } // +++ return new Response(null, { status: 200 }); } ``` 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 , // The formatted comment, as an HTML string body: "
...
", author: { id: "aurélien@example.com", info: { /* Custom user info you have resolved */ }, }, }, } ```
```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 , // The formatted comment, as an HTML string body: "
...
", author: { id: "aurélien@example.com", info: { /* Custom user info you have resolved */ }, }, }, // More comments //... ], } ```
A [`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client) Node.js client. An object passed from a webhook event, specifically the [`ThreadNotificationEvent`](/docs/platform/webhooks#Thread-notification). [Learn more about setting this up](#Setup). A number of options to customize the format of the comments, adding user info, room info, and styles. 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). 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). 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). 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). Inline styles to apply to the comment body container. Inline styles to apply to the paragraph block. Inline styles to apply to the code element. Inline styles to apply to the strong element. Inline styles to apply to the mention element. Inline styles to apply to the link element. #### 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. 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. ```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).
An email showing a text mention in a text editor document
This screenshot shows a ready-made template from our [Text Editor + Resend](/examples/collaborative-text-editor-emails/nextjs-tiptap-emails-resend) examples. 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 = (
@{emailData.mention.author.id} at {emailData.mention.createdAt}
{emailData.mention.content}
); } // 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 = (
@{emailData.mention.author.id} at {emailData.mention.createdAt}
{emailData.mention.content}
); // Send your email // ... } // +++ return new Response(null, { status: 200 }); } ``` 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 , // The formatted content, pass it to React `children` content: { /* ... */} author: { id: "vincent@example.com", info: { /* Custom user info you have resolved */ } } }, } ``` A [`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client) Node.js client. An object passed from a webhook event, specifically the [`TextMentionNotificationEvent`](/docs/platform/webhooks#TextMention-notification). [Learn more about setting this up](#Setup). A number of options to customize the format of the content, adding user info, room info, and styles. 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). 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). 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). 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). The mention and its surrounding text container The text element. ReactNode`} > The mention element. #### 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. 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. ```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 }) =>
{children}
, Text: ({ children }) => ( {children} ), // `user` and `group` are the optional data returned from `resolveUsers` and `resolveGroupsInfo` Mention: ({ element, user, group }) => ( @{user?.name ?? group?.name ?? element.id} ), }, // +++ } ); // { 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 = (
@{emailData.mention.author.id} at {emailData.mention.createdAt}
{emailData.mention.content}
); } // 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 = `
@${emailData.mention.author.id} at ${emailData.mention.createdAt}
${emailData.mention.content}
`; // Send your email // ... } // +++ return new Response(null, { status: 200 }); } ``` 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 , // The formatted content, as an HTML string content: { /* ... */} author: { id: "vincent@example.com", info: { /* Custom user info you have resolved */ } } }, } ``` A [`Liveblocks`](/docs/api-reference/liveblocks-node#Liveblocks-client) Node.js client. An object passed from a webhook event, specifically the [`TextMentionNotificationEvent`](/docs/platform/webhooks#TextMention-notification). [Learn more about setting this up](#Setup). A number of options to customize the format of the content, adding user info, room info, and styles. 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). 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). 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). 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). Inline styles to apply to the mention container. It's a `
` element under the hood. Inline styles to apply to the code element. Inline styles to apply to the strong element. Inline styles to apply to the mention element. Inline styles to apply to the link element. #### 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. 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. ```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` // ... } ); ``` Returns the value you return from the `doc` callback. The ID of the room to use. The [Liveblocks client](/docs/api-reference/liveblocks-node#Liveblocks-client) to use. Optional. The Lexical nodes used in the document. Will extend the default schema which uses Liveblocks mentions and Liveblocks comments. ### 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 // ... }); // +++ } ); ``` _Nothing_ Callback function where you should handle your modifications. ##### 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(); } // +++ ); ``` Returns the text retrieved from the document. _None_ #### 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(); } // +++ ); ``` Your editor’s Lexical state. _None_ #### 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(); } // +++ ); ``` Your headless Lexical editor. _None_ #### 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(); } // +++ ); ``` A serialized JSON object representation of your document. _None_ #### 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(); } // +++ ); ``` Returns the markdown string. _None_ --- 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 // ... } ); ``` Returns the value you return from the `api` callback. The ID of the room to use. The [Liveblocks client](/docs/api-reference/liveblocks-node#Liveblocks-client) to use. 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. Optional. The [field](/docs/api-reference/liveblocks-react-tiptap#Multiple-editors) to use for the document. Defaults to `default`. ### 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"); }); // +++ } ); ``` _Nothing_ `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). #### 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 // ... }); // +++ } ); ``` Returns the text retrieved from the document. Optional. The separator to use for blocks, e.g. `
`. Defaults to `\n\n`.
Optional. The serializers to use for text. Defaults to `{}`.
#### 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( { client, roomId: "test-room", }, async (api) => { // +++ await api.setContent(exampleDoc); // +++ return JSON.stringify(api.toJSON()); } ); ``` _Nothing_ The content to replace your document. #### api.getEditorState Returns the current ProseMirror state. ```ts const editorState = await withProsemirrorDocument( { roomId: "my-room-id", client: liveblocks }, async (api) => { // +++ return api.getEditorState(); // +++ } ); ``` Your editor’s ProseMirror state. _None_ #### 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(); // +++ } ); ``` Your editor’s serialized JSON state. _None_ #### api.clearContent Clears the content of the document. ```ts await withProsemirrorDocument( { roomId: "my-room-id", client: liveblocks }, async (api) => { // +++ return api.clearContent(); // +++ } ); ``` _Nothing_ _None_ #### 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(); // +++ } ); ``` Returns the markdown string. Optional. A markdown serializer to use. By default it uses the `defaultMarkdownSerializer` from [prosemirror-markdown](https://github.com/prosemirror/prosemirror-markdown). ##### 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", }); ``` Learn how to [get started with ID tokens](/docs/authentication). 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"], // +++ }); ``` Learn more about [ID token permissions](/docs/authentication#permissions). ##### 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. 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.
Auth diagram
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" ); ``` Learn how to [get started with access tokens](/docs/authentication/access-token). 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); ``` Learn more about [access token permissions](/docs/authentication/access-token#permissions). 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. 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.
Auth diagram
To implement your back end, follow these steps: Create a session ```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. Decide which permissions to allow this session ```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); ``` 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. Authorize the session 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 }); ``` ##### 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[] = []; 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[] = []; 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 ( // +++ prewarmRoom(roomId)}> // +++ {roomId} ); } ``` `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; }; } } // { // 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; }; } } // { // 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; }; } } // Create the initial conflict-free data const initialStorage: LiveObject = 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; }; } } // 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 } `; 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 } `; 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(), // 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.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 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", // The time the comment was created createdAt: new Date(), }, }); ``` #### 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: { body: { version: 1, content: [ /* The comment's body text goes here, see below */ ], }, userId: "alicia@example.com", createdAt: new Date(), // Optional }, }); // { 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 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, // ... }, }); ``` ```ts 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: { version: 1, content: [ /* The comment's body text goes here, see above */ ], }, // The ID of the user that edited the comment userId: "alicia@example.com", // Optional, the time the comment was edited editedAt: new Date(), }, }); // { type: "comment", threadId: "th_d75sF3...", ... } console.log(editedComment); ``` #### 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); ``` 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). ### 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 }) => `

${children}

`, // Optional, override the `text` element text: ({ element }) => element.bold ? `${element.text}` : `${element.text}`, // Optional, override the `link` element link: ({ element, href }) => `${element.url}`, // Optional, override the `mention` element. // `user` and `group` are the optional data returned from `resolveUsers` and `resolveGroupsInfo` mention: ({ element, user, group }) => `${element.id}`, }, // 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, })); }, }); ``` 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). #### 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" }]; }, }); // "Hello @Marc from // https://liveblocks.io" await stringifyCommentBody(comment.body, { format: "html", resolveUsers() { return [{ name: "Marc" }]; }, }); // "Hello @Marc from // https://liveblocks.io" await stringifyCommentBody(comment.body, { format: "html", mention: ({ element, user }) => `${user.name}`, resolveUsers() { return [{ name: "Marc", profileUrl: "https://example.com" }]; }, }); ``` ### WebhookHandler [#WebhookHandler] 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. 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 (
// +++ // +++
); } ``` 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 (
// +++ // +++
); } ``` 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
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 (
// +++ // +++
); } ``` #### 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 ( ); } ``` #### Props [#FloatingComposer-props] The metadata of the thread to create. The event handler called when the composer is submitted. The composer’s initial value. Whether the composer is collapsed. Setting a value will make the composer controlled. The event handler called when the collapsed state of the composer changes. Whether the composer is initially collapsed. Setting a value will make the composer uncontrolled. Whether the composer is disabled. Whether to focus the composer on mount. Override the component’s strings. ### FloatingThreads Displays floating [`Thread`][] components below text highlights in the editor. ```tsx highlight="3" ```
FloatingThreads
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 (
// +++ // +++
); } ``` The `FloatingThreads` component automatically excludes resolved threads from display. Any resolved threads passed in the threads list will not be shown. #### 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 ( <> ); } ``` ```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 ( ); } // +++ // +++ return ( ); // +++ } 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
// +++ // +++
``` #### 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 ``` 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"; ( ), }} // +++ />; ``` 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"; ( // +++
{props.thread.comments.map((comment) => ( ))}
// +++ ), }} />; ``` #### Props [#FloatingThreads-props] The threads to display. Override the component’s components. Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component. ### AnchoredThreads Displays a list of [`Thread`][] components vertically alongside the editor. ```tsx highlight="3" ```
AnchoredThreads
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 (
// +++ // +++
); } ``` The `AnchoredThreads` component automatically excludes resolved threads from display. Any resolved threads passed in the threads list will not be shown. #### 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 ( <> ); } ``` ```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 ( ); } // +++ // +++ return ( ); // +++ } 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
// +++ // +++
``` #### 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 ``` 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"; ( ), }} // +++ />; ``` 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"; ( // +++
{props.thread.comments.map((comment) => ( ))}
// +++ ), }} />; ``` ##### 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] The threads to display. Override the component’s components. Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component. ### 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 ( ); } ``` #### Props [#HistoryVersionPreview-props] The version of the editor content to preview. Callback function called when the user chooses to restore this version. 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 (
// +++ // +++
); } ``` A number of options can be applied to BlockNote and Liveblocks. ```tsx const editor = useCreateBlockNoteWithLiveblocks( { // +++ animations: false, trailingBlock: false, // +++ // Other BlockNote options // ... }, { // +++ initialContent: "

Hello world

", field: "editor-one", // +++ // Other Liveblocks options // ... } ); ``` Returns a [BlockNote editor](https://www.blocknotejs.org/docs/editor-basics/setup) with collaborative Liveblocks features. Options to apply to BlockNote. [Learn more](https://www.blocknotejs.org/docs/editor-basics/setup#usecreateblocknote-hook). Options to apply to Liveblocks. The initial content for the editor, if it’s never been set. [Learn more](#Setting-initial-content). 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). 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). Enable comments in the editor. Enable mentions in the editor. #### 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: "

Hello world

", // +++ } ); // ... } ``` #### 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 (
// +++ // +++
); } function TextEditor({ field }: { field: string }) { const editor = useCreateBlockNoteWithLiveblocks( {}, { // +++ field, // +++ } ); return (
); } ``` #### 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 ( <> // +++ // +++ ); } // +++ function Threads({ editor }: { editor: BlockNoteEditor }) { const { threads } = useThreads(); return ; } // +++ ``` ### 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 (
// +++ {!ready ?
Loading...
: } // +++
); } ``` ## 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" ``` {/* 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 ( // +++ // +++ } placeholder={
Enter some text...
} ErrorBoundary={LexicalErrorBoundary} />
); } ``` Annotations associated with resolved threads are hidden by default on the editor. 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 ( } placeholder={
Enter some text...
} ErrorBoundary={LexicalErrorBoundary} />
); } ``` 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 // +++ // +++ ```
Toolbar
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 ( // +++ // +++ } placeholder={
Enter some text...
} ErrorBoundary={LexicalErrorBoundary} />
); } ``` #### 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"; I'm at the start} after={ } 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 ( Hello world ); } ``` 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 ( // +++ // +++ ); } ``` 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 ( // +++ } shortcut="CMD-H" onClick={() => console.log("help")} /> B️} active={isTextFormatActive(editor, "bold")} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")} /> // +++ ); } ``` To learn more about the different components, read more below. #### Props [#Toolbar-props] The Lexical editor. 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. The content to display at the start of the toolbar. Any `ReactNode` or `Toolbar.*` components work inside. The content to display at the end of the toolbar. Any `ReactNode` or `Toolbar.*` components work inside. #### 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"; console.log("Clicked")} /> ; ``` 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" // Tooltip says "Question [⌘+Q]" // Custom icon, replaces the name in the button ?
} onClick={/* ... */} /> // Using a Liveblocks icon, replaces the name in the button } onClick={/* ... */} /> // Passing children visually replaces the `name` and `icon` ? Ask a question // Props are passed to the inner `button` console.log("Hovered")} /> ``` ##### Props [#ToolbarButton-props] The name of this button displayed in its tooltip. Will also be displayed in the button if no `icon` or `children` are passed. An optional icon displayed in this button. An optional keyboard shortcut displayed in this button’s tooltip. Common shortcuts such will be replaced by their symbols, for example `CMD` → `⌘`. #### 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"; editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")} /> ; ``` 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 ( 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" // Tooltip says "Highlight [⌘+H]" // Custom icon, replaces the name in the button 🖊} active={/* ... */} onClick={/* ... */} /> // Using a Liveblocks icon, replaces the name in the button } active={/* ... */} onClick={/* ... */} /> // Passing children visually replaces the `name` and `icon` 🖊️Highlight // Props are passed to the inner `button` console.log("Hovered")} /> ``` ##### Props [#ToolbarToggle-props] The name of this button displayed in its tooltip. Will also be displayed in the button if no `icon` or `children` are passed. Whether the button is toggled. An optional icon displayed in this button. An optional keyboard shortcut displayed in this button’s tooltip. Common shortcuts such will be replaced by their symbols, for example `CMD` → `⌘`. #### 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"; ; ``` ##### 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"; [ ...defaultItems, { name: "Code block", icon:
❮ ❯
, // Optional isActive: (editor) => isBlockNodeActive(editor, $isCodeNode), setActive: (editor) => $setBlocksType($getSelection(), () => $createCodeNode()), }, ]} />
; ``` ##### 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"; [ ...defaultItems, { name: "Code block", // +++ label:
Code
, // Optional, overwrites `icon` + `name` // +++ isActive: (editor) => isBlockNodeActive(editor, $isCodeNode), setActive: (editor) => $setBlocksType($getSelection(), () => $createCodeNode()), }, ]} />
; ``` 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"; defaultItems.map((item) => { let label; if (item.name === "Text") { label = Regular text; } if (item.name === "Heading 1") { label = ( Heading 1 ); } if (item.name === "Heading 2") { label = ( Heading 2 ); } if (item.name === "Heading 3") { label = ( Heading 3 ); } if (item.name === "Blockquote") { label = ( Blockquote ); } return { ...item, label, icon: null, // Hide all icons }; }) } />; ``` ##### Props [#ToolbarBlockSelector-props] 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. #### 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.SectionHistory Adds a section containing _undo_ and _redo_ buttons. Can also be placed inside [`FloatingToolbar`][]. ```tsx import { Toolbar } from "@liveblocks/react-lexical"; // +++ // +++ ; ``` #### 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.SectionCollaboration Adds a section containing an _add comment_ button. Can also be placed inside [`FloatingToolbar`][]. ```tsx import { Toolbar } from "@liveblocks/react-lexical"; // +++ // +++ ; ``` ### 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 // +++ // +++ ```
FloatingToolbar
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 ( // +++ // +++ } placeholder={
Enter some text...
} ErrorBoundary={LexicalErrorBoundary} />
); } ``` #### 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 ``` #### 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"; I'm at the start} after={ } 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 ( Hello world ); } ``` 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 ( // +++ // +++ ); } ``` 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 ( // +++ } shortcut="CMD-H" onClick={() => console.log("help")} /> B️} active={isTextFormatActive(editor, "bold")} onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")} /> // +++ ); } ``` To learn more about the different components, read more under [`Toolbar`][]. #### Props [#FloatingToolbar-props] The Lexical editor. The vertical position of the floating toolbar. The vertical offset of the floating toolbar from the selection. 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. The content to display at the start of the toolbar. Any `ReactNode` or `Toolbar.*` components work inside. The content to display at the end of the toolbar. Any `ReactNode` or `Toolbar.*` components work inside. ### FloatingComposer Displays a [`Composer`][] near the current Lexical selection, allowing you to create threads. ```tsx highlight="3" ```
FloatingComposer
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 ( } placeholder={
Enter some text...
} ErrorBoundary={LexicalErrorBoundary} />
); } ``` 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 ( ); } ``` #### 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 ``` 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"; ( ), }} // +++ />; ``` 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"; ( Send ), // +++ }} />; ``` 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"; ( { event.preventDefault(); const thread = createThread({ body: comment.body, attachments: comment.attachments, metadata: ..., }); editor.dispatchCommand(ATTACH_THREAD_COMMAND, thread.id); }} // +++ > Send ) }} />; ``` #### Props [#FloatingComposer-props] The metadata of the thread to create. The event handler called when the composer is submitted. The composer’s initial value. Whether the composer is collapsed. Setting a value will make the composer controlled. The event handler called when the collapsed state of the composer changes. Whether the composer is initially collapsed. Setting a value will make the composer uncontrolled. Whether the composer is disabled. Whether to focus the composer on mount. Override the component’s strings. Override the component’s components. Override the [`Composer`](/docs/api-reference/liveblocks-react-ui#Composer) component. ### FloatingThreads Displays floating [`Thread`][] components below text highlights in the editor. ```tsx highlight="3" ```
FloatingThreads
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 ( // +++ // +++ } placeholder={
Enter some text...
} ErrorBoundary={LexicalErrorBoundary} />
); } ``` Should be nested inside [`LiveblocksPlugin`][]. The `FloatingThreads` component automatically excludes resolved threads from display. Any resolved threads passed in the threads list will not be shown. #### 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 ( <> ); } ``` ```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 ; } // +++ // +++ return ; // +++ } 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 // +++ // +++ ``` #### 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 ``` 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"; ( ), }} // +++ />; ``` 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"; ( // +++
{props.thread.comments.map((comment) => ( ))}
// +++ ), }} />; ``` #### Props [#FloatingThreads-props] The threads to display. Override the component’s components. Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component. ### AnchoredThreads Displays a list of [`Thread`][] components vertically alongside the editor. ```tsx highlight="3" ```
AnchoredThreads
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 ( // +++ // +++ } placeholder={
Enter some text...
} ErrorBoundary={LexicalErrorBoundary} />
); } ``` Should be nested inside [`LiveblocksPlugin`][]. The `AnchoredThreads` component automatically excludes resolved threads from display. Any resolved threads passed in the threads list will not be shown. #### 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 ( <> ); } ``` ```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 ; } // +++ // +++ return ; // +++ } 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 // +++ // +++ ``` #### 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 ``` 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"; ( ), }} // +++ />; ``` 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"; ( // +++
{props.thread.comments.map((comment) => ( ))}
// +++ ), }} />; ``` ##### 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] The threads to display. Override the component’s components. Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component. ### 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 `` 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 ( ); } ``` #### Props The version of the editor content to preview. Callback function called when the user chooses to restore this version. 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 ( // +++ {!ready ? (
Loading...
) : ( } placeholder={
Enter some text...
} ErrorBoundary={LexicalErrorBoundary} /> )} // +++
); } ``` ### 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); ``` The ID of the thread. 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"); ``` The Lexical editor. The Lexical text format to check for in the current selection. #### 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 ( B️} // +++ 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); ``` The Lexical editor. Function that passes the current node, helping you check if the current block node is active. Helpful in combination with `$is___Node` functions. #### 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"; [ ...defaultItems, { name: "Code block", icon:
❮ ❯
, // +++ isActive: (editor) => isBlockNodeActive(editor, $isCodeNode), // +++ setActive: (editor) => $setBlocksType($getSelection(), () => $createCodeNode()), }, ]} />
; ``` ## 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 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. 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 (
); } ``` 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 (
// +++ // +++
); } ``` 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
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 (
// +++ // +++
); } ``` #### 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"; I'm at the start} after={ } 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 ( Hello world ); } ``` 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 ( // +++ // +++ ); } ``` 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 ( // +++ } shortcut="CMD-H" onClick={() => console.log("help")} /> 🖊️} active={editor?.isActive("highlight") ?? false} onClick={() => editor?.chain().focus().toggleHighlight().run()} disabled={!editor?.can().chain().focus().toggleHighlight().run()} /> // +++ ); } ``` To learn more about the different components, read more below. #### Props [#Toolbar-props] The Tiptap editor. 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. The content to display at the start of the toolbar. Any `ReactNode` or `Toolbar.*` components work inside. The content to display at the end of the toolbar. Any `ReactNode` or `Toolbar.*` components work inside. #### 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"; console.log("Clicked")} /> ; ``` 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" // Tooltip says "Question [⌘+Q]" // Custom icon, replaces the name in the button ?} onClick={/* ... */} /> // Using a Liveblocks icon, replaces the name in the button } onClick={/* ... */} /> // Passing children visually replaces the `name` and `icon` ? Ask a question // Props are passed to the inner `button` console.log("Hovered")} /> ``` ##### Props [#ToolbarButton-props] The name of this button displayed in its tooltip. Will also be displayed in the button if no `icon` or `children` are passed. An optional icon displayed in this button. An optional keyboard shortcut displayed in this button’s tooltip. Common shortcuts such will be replaced by their symbols, for example `CMD` → `⌘`. #### 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"; editor?.chain().focus().toggleHighlight().run()} /> ; ``` 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 ( 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" // Tooltip says "Highlight [⌘+H]" // Custom icon, replaces the name in the button 🖊} active={/* ... */} onClick={/* ... */} /> // Using a Liveblocks icon, replaces the name in the button } active={/* ... */} onClick={/* ... */} /> // Passing children visually replaces the `name` and `icon` 🖊️Highlight // Props are passed to the inner `button` console.log("Hovered")} /> ``` ##### Props [#ToolbarToggle-props] The name of this button displayed in its tooltip. Will also be displayed in the button if no `icon` or `children` are passed. Whether the button is toggled. An optional icon displayed in this button. An optional keyboard shortcut displayed in this button’s tooltip. Common shortcuts such will be replaced by their symbols, for example `CMD` → `⌘`. #### 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"; ; ``` ##### 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"; [ ...defaultItems, { name: "Code block", icon:
❮ ❯
, // Optional isActive: (editor) => editor.isActive("codeBlock"), setActive: (editor) => editor.chain().focus().clearNodes().toggleCodeBlock().run(), }, ]} />
; ``` ##### 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"; [ ...defaultItems, { name: "Code block", // +++ label:
Code
, // Optional, overwrites `icon` + `name` // +++ isActive: (editor) => editor.isActive("codeBlock"), setActive: (editor) => editor.chain().focus().clearNodes().toggleCodeBlock().run(), }, ]} />
; ``` 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"; defaultItems.map((item) => { let label; if (item.name === "Text") { label = Regular text; } if (item.name === "Heading 1") { label = ( Heading 1 ); } if (item.name === "Heading 2") { label = ( Heading 2 ); } if (item.name === "Heading 3") { label = ( Heading 3 ); } if (item.name === "Blockquote") { label = ( Blockquote ); } return { ...item, label, icon: null, // Hide all icons }; }) } />; ``` ##### Props [#ToolbarBlockSelector-props] 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. #### 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.SectionHistory Adds a section containing _undo_ and _redo_ buttons. Can also be placed inside [`FloatingToolbar`][]. ```tsx import { Toolbar } from "@liveblocks/react-tiptap"; // +++ // +++ ; ``` #### 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.SectionCollaboration Adds a section containing an _add comment_ button. Can also be placed inside [`FloatingToolbar`][]. ```tsx import { Toolbar } from "@liveblocks/react-tiptap"; // +++ // +++ ; ``` ### 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
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 (
// +++ // +++
); } ``` #### 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 ``` #### 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"; I'm at the start} after={ } 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 ( Hello world ); } ``` 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 ( // +++ // +++ ); } ``` 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 ( // +++ } shortcut="CMD-H" onClick={() => console.log("help")} /> 🖊️} active={editor?.isActive("highlight") ?? false} onClick={() => editor?.chain().focus().toggleHighlight().run()} disabled={!editor?.can().chain().focus().toggleHighlight().run()} /> // +++ ); } ``` To learn more about the different components, read more under [`Toolbar`][]. #### Props [#FloatingToolbar-props] The Tiptap editor. The vertical position of the floating toolbar. The vertical offset of the floating toolbar from the selection. 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. The content to display at the start of the toolbar. Any `ReactNode` or `Toolbar.*` components work inside. The content to display at the end of the toolbar. Any `ReactNode` or `Toolbar.*` components work inside. ### FloatingComposer Displays a [`Composer`][] near the current Tiptap selection, allowing you to create threads. ```tsx highlight="3" ```
FloatingComposer
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 (
// +++ // +++
); } ``` #### 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 ( ); } ``` #### 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 ``` 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"; ( ), }} // +++ />; ``` 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"; ( // +++ Send // +++ ), }} />; ``` 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"; ( { event.preventDefault(); const thread = createThread({ body: comment.body, attachments: comment.attachments, metadata: ..., }); editor.commands.addComment(thread.id); }} // +++ > Send ), }} />; ``` #### Props [#FloatingComposer-props] The metadata of the thread to create. The event handler called when the composer is submitted. The composer’s initial value. Whether the composer is collapsed. Setting a value will make the composer controlled. The event handler called when the collapsed state of the composer changes. Whether the composer is initially collapsed. Setting a value will make the composer uncontrolled. Whether the composer is disabled. Whether to focus the composer on mount. Override the component’s strings. Override the component’s components. Override the [`Composer`](/docs/api-reference/liveblocks-react-ui#Composer) component. ### FloatingThreads Displays floating [`Thread`][] components below text highlights in the editor. ```tsx highlight="3" ```
FloatingThreads
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 (
// +++ // +++
); } ``` The `FloatingThreads` component automatically excludes resolved threads from display. Any resolved threads passed in the threads list will not be shown. #### 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 ( <> ); } ``` ```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 ( ); } // +++ // +++ return ( ); // +++ } 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
// +++ // +++
``` #### 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 ``` 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"; ( ), }} // +++ />; ``` 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"; ( // +++
{props.thread.comments.map((comment) => ( ))}
// +++ ), }} />; ``` #### Props [#FloatingThreads-props] The threads to display. Override the component’s components. Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component. ### AnchoredThreads Displays a list of [`Thread`][] components vertically alongside the editor. ```tsx highlight="3" ```
AnchoredThreads
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 (
// +++ // +++
); } ``` The `AnchoredThreads` component automatically excludes resolved threads from display. Any resolved threads passed in the threads list will not be shown. #### 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 ( <> ); } ``` ```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 ( ); } // +++ // +++ return ( ); // +++ } 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
// +++ // +++
``` #### 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 ``` 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"; ( ), }} // +++ />; ``` 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"; ( // +++
{props.thread.comments.map((comment) => ( ))}
// +++ ), }} />; ``` ##### 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] The threads to display. Override the component’s components. Override the [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component. ### 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 ( ); } ``` #### Props [#HistoryVersionPreview-props] The version of the editor content to preview. Callback function called when the user chooses to restore this version. 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] 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. 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 ``` 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 (
// +++ // +++
); } ``` #### 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 ( ); } ``` You can also pass a prompt to the `askAi` command and it will directly request it when opening the toolbar. ```tsx ``` #### 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 Suggested Fix mistakes Emojify }> Continue writing } // +++ /> ``` Doing so will override the default suggestions, instead you can use a function to keep them while adding your own. ```tsx ( <> {children} // +++ Custom Custom suggestion )} /> ``` #### Props [#AiToolbar-props] The Tiptap editor. The vertical offset of the AI toolbar from the selection. The prompt suggestions to display below the AI toolbar. #### AiToolbar.Suggestion A prompt suggestion displayed below the AI toolbar. ```tsx import { AiToolbar } from "@liveblocks/react-tiptap"; // +++ Fix mistakes // +++ } />; ``` 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 Translate to French // "Emojify" is displayed in the suggestion but "Add emojis to the text" is used as the prompt Emojify // Custom icon ?}>Explain // Using a Liveblocks icon }>Explain ``` ##### Props [#AiToolbarSuggestion-props] The suggestion’s label, used as the prompt if the `prompt` prop is not set. An optional icon displayed before the label. The prompt to use instead of the label. #### AiToolbar.SuggestionsLabel A label to describe a group of prompt suggestions displayed in the AI toolbar. ```tsx import { AiToolbar } from "@liveblocks/react-tiptap"; // +++ Translation // +++ Translate in French Translate in English } />; ``` #### AiToolbar.SuggestionsSeparator A separator between groups of prompt suggestions displayed in the AI toolbar. ```tsx import { AiToolbar } from "@liveblocks/react-tiptap"; Translate in French Translate in English // +++ // +++ Custom 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 (
); } ``` A number of options can be applied. ```tsx const liveblocks = useLiveblocksExtension({ // +++ initialContent: "Hello world", field: "editor-one", // +++ // Other options // ... }); ``` Returns a Liveblocks [Tiptap extension](https://tiptap.dev/docs/editor/core-concepts/extensions#create-a-new-extension). The initial content for the editor, if it’s never been set. [Learn more](#Setting-initial-content). 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). 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). Enable comments in the editor. Enable mentions in the editor. Enable AI in the editor and optionally customize configuration options. #### 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: "

Hello world

", // +++ }); // ... } ``` #### 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 (
// +++ // +++
); } function TextEditor({ field }: { field: string }) { // +++ const liveblocks = useLiveblocksExtension({ field }); // +++ const editor = useEditor({ extensions: [ liveblocks, // ... ], }); return (
); } ``` #### 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 ( <> // +++ // +++ ); } // +++ function Threads({ editor }: { editor: Editor }) { const { threads } = useThreads(); return ; } // +++ ``` #### Customizing AI components [@badge=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. 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] 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. 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 (
// +++ {!ready ?
Loading...
: } // +++
); } ``` ## 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 ```
AI Chat
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 ; } ``` ##### 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 ( ); } ``` 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 ( I'm an empty chat! }} // +++ /> ); } ``` 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 (
Suggestions
// +++ // +++ ), }} /> ); } ``` ##### 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
Loading...
; } if (error) { return
Error: {error.message}
; } return (
    // +++ {chats.map((chat) => (
  • ))} // +++
// +++ // +++
); } ``` ##### 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
Loading...
; } if (error) { return
Error: {error.message}
; } return (
// +++

{chat.title || "Untitled chat"}

// +++
); } ``` ##### 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 ( ); } ``` ##### 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 ( ); } ``` ```tsx title="Set variable in Tailwind CSS" isCollapsable isCollapsed import { CSSProperties } from "react"; import { AiChat } from "@liveblocks/react-ui"; function Chat() { return ( ); } ``` ##### 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 ( ); } ``` ##### 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 ( ); } ``` ```tsx title="Set variable in Tailwind CSS" isCollapsable isCollapsed import { CSSProperties } from "react"; import { AiChat } from "@liveblocks/react-ui"; function Chat() { return ( ); } ``` ##### 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

{children}

, // Example: Use an existing component for quotes Blockquote: ({ children }) => {children}, // Example: Use `next/link` instead of default `` tag Link: ({ children, href }) => {children}, // Example: Use an external library to add syntax highlighting to code blocks CodeBlock: ({ language, code }) => ( {code} ), // `Heading`, `Inline`, `List`, `Table`, `Image`, `Separator`, etc. // ... }, // +++ }} /> ``` ##### Props [#AiChat-props] The unique identifier for the chat. Each chat is stored permanently and is only visible to the authenticated user who created it. Whether to automatically focus the composer input when the chat loads. Defaults to `false`. 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). 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. Object mapping tool names to tool definitions that should be available in this chat. The event handler called when the composer is submitted. The layout mode for the chat. Use `'inset'` (default) for standalone chats, or `'compact'` for embedded scenarios like pop-up windows. Advanced customization options for overriding default chat behavior and styling. Custom components to override specific parts of the chat UI, such as the `Empty` placeholder component or Markdown components. The time, in milliseconds, before an AI response will timeout. Defaults to 30_000. Whether to show reasoning. Defaults to `true`. If set to `'during'`, reasoning will only be shown during reasoning. Whether to show retrievals. Defaults to `true`. If set to `'during'`, retrievals will only be shown during retrieval. Whether to show sources. Defaults to `true`. CSS class name to apply to the chat container. Inline styles to apply to the chat container. Useful for setting CSS custom properties. ###### components [#AiChat-components] Override specific parts of `AiChat` with custom components. The component used to render the empty state of the chat. Defaults to nothing. The component used to render the loading state of the chat. Defaults to a loading spinner. The components used to render Markdown content. The component used to render paragraphs. ReactNode`} > The component used to render inline elements (bold, italic, strikethrough, and inline code). ReactNode`} > The component used to render links. ReactNode`} > The component used to render headings. The component used to render blockquotes. ReactNode`} > The component used to render code blocks. ReactNode`} > The component used to render images. ReactNode`} > The component used to render lists. ReactNode`} > The component used to render tables. The component used to render separators. #### 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 ``` 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 ( <> { const { temperature, condition } = await __getWeather__( args.location ); return { data: { temperature, condition } }; }, render: ({ result }) => ( // +++ {result ? (
{result.temperature}°F - {result.condition}
) : null}
// +++ ), })} /> ); } ``` Optionally, you can provide a `title` and `icon` to render the UI differently. ```tsx // Title render: () => // Title and a collapsible description render: () => We've booked the event!, // Title and emoji icon render: () => , // Title and icon component render: () => } />, // Props are passed to the inner `div` render: () => ( console.log("Hovered")} /> ), ``` ##### Props [#AiTool-props] The title to display for the tool. If not provided, the tool name will be formatted as a human-readable string. Icon to display alongside the tool title. Can be an emoji string, React component, or any ReactNode. Content to display inside the tool container. Typically used for tool-specific UI or descriptions. The visual appearance of the tool. The `"block"` variant (default) displays the tool as a block with a border. Whether the tool content should be collapsed. When collapsed, only the title and icon are visible. Callback fired when the collapsed state changes. Use this to control the collapsed state externally. 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. CSS class name to apply to the tool container. Inline styles to apply to the tool container. 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 /* ... */} cancel={() => /* ... */} /> ``` {/* 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 ( {!result.data ? ( // +++ { await deleteFile(fileName); return { data: { deletedFileName: fileName }, }; }} > Are you sure you want to delete {args.fileName}? ) : ( // +++
Deleted {result.data.deletedFileName}
)}
); }, }); function App() { return ( <> ); } ``` `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] 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" }` Function called when the user clicks the cancel button. Content to display in the confirmation UI. Typically a question or description of the action being confirmed. The visual appearance of the confirmation UI. Override the component’s strings. It can be used the change the "confirm" and "cancel" labels. 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 ``` To use, simply include `` inside an `` 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 ( <> { toggleTodo(id); }, render: () => ( // +++ // +++ ), })} /> ); } ``` ###### 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
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) => ( ))} ); } ``` ##### 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 ``` 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}`. // Only show the last comment, and all the older ones to fit the limit. // Show as many old comments as new ones to fit the limit. ``` ##### 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 { console.log("Open details"); }} > Details { console.log("Move comment"); }} > Move } /> ``` 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 { // +++ return ( <> { // +++ openDetails(comment.id); // +++ }} > Details // +++ {/* 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 { openDetails(comment.id); }} // +++ icon={} // +++ > Details ``` [`Comment`](/docs/api-reference/liveblocks-react-ui#Comment) offers the same as `Thread`’s `commentDropdownItems` but named `dropdownItems` instead. ##### Props [#Thread-props] The thread to display. How to show or hide the composer to reply to the thread. How to show or hide the actions. Whether to show reactions. Whether to show attachments. Whether to show the composer’s formatting controls. Whether to blur the composer editor when the composer is submitted. Whether to show the action to resolve the thread. The maximum number of comments to show. Whether to indent the comments’ content. Whether to show deleted comments. Add (or change) items to display in the comment’s dropdown. The event handler called when the composer is submitted. The event handler called when changing the resolved status. The event handler called when the thread is deleted. A thread is deleted when all its comments are deleted. The event handler called when a comment is edited. The event handler called when a comment is deleted. The event handler called when clicking on a comment’s author. The event handler called when clicking on a mention. The event handler called when clicking on a comment’s attachment. Override the component’s strings. {/* TODO: Document classes and data attributes */} #### Composer Displays a composer for creating threads or comments. ```tsx ```
Composer
By default, submitting the composer will create a new thread. ```tsx import { Composer } from "@liveblocks/react-ui"; // Creates a new thread function Component() { return ; } ``` ##### 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 ( ); } ``` ###### 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; }; // +++ // 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 ; } ``` ##### 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 ; } ``` ##### 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 ( { 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] The ID of the thread to reply to or to edit a comment in. The ID of the comment to edit. The metadata of the thread to create. The event handler called when the composer is submitted. Whether to blur the composer editor when the composer is submitted. The composer’s initial value. The composer’s initial attachments. Whether the composer is collapsed. Setting a value will make the composer controlled. The event handler called when the collapsed state of the composer changes. Whether to show and allow adding attachments. Whether to show formatting controls (e.g. a floating toolbar with formatting toggles when selecting text) Whether the composer is initially collapsed. Setting a value will make the composer uncontrolled. Whether the composer is disabled. Whether to focus the composer on mount. Override the component’s strings. #### Comment Displays a single comment. ```tsx ```
Comment
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) => ( ))} ); } ``` ##### 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) => ( ))} ); } // Renders a list of custom thread components function Component() { const { threads } = useThreads(); return ( <> {threads.map((thread) => ( ))} ); } ``` ##### Props [#Comment-props] The comment to display. How to show or hide the actions. Whether to show reactions. Whether to show attachments. Whether to show the composer’s formatting controls when editing the comment. Whether to indent the comment’s content. Whether to show the comment if it was deleted. If set to `false`, it will render deleted comments as `null`. Add (or change) items to display in the comment’s dropdown. The event handler called when the comment is edited. The event handler called when the comment is deleted. The event handler called when clicking on the author. The event handler called when clicking on a mention. The event handler called when clicking on a comment’s attachment. Override the component’s strings. ### 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. 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). #### 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 ; ``` 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 , MentionSuggestions: () => ( ), Link: () => , }} /> ``` 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 ) { event.preventDefault(); // Create a new thread // +++ const thread = createThread({ body, attachments, metadata: {}, }); // +++ } return ( // +++ Create thread // +++ ); } // Render a mention in the composer's editor, e.g. "@Emil Joyce" function Mention({ mention }: CommentBodyMentionProps) { return @{mention.id}; } // Render a list of mention suggestions, used after typing "@" in the editor function MentionSuggestions({ mentions, selectedMentionId, }: ComposerEditorMentionSuggestionsProps) { return ( {mentions.map((mention) => { switch (mention.kind) { case "user": return ( ); case "group": return ( ); } })} ); } // Render a single mention suggestion from a `userId` function UserMentionSuggestion({ userId }: { userId: string }) { const { user } = useUser(userId); return ( {user.name} {user.name} ); } // Render a single mention suggestion from a `groupId` function GroupMentionSuggestion({ groupId }: { groupId: string }) { const { group } = useGroupInfo(groupId); return ( {group.name} {group.name} ); } // Render a link in the composer's editor, e.g. "https://liveblocks.io" function Link({ href, children }: CommentBodyLinkProps) { return {children}; } ``` ##### 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 { // Mutate your comments // ... }} > {/* ... */} ``` The composer’s initial attachments. Whether to create attachments when pasting files into the editor. 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`. Whether to blur the editor when the form is submitted. The event handler called when the form is submitted. Whether the composer is disabled. Replace the rendered element by the one passed as a child. ##### Composer.Editor [#primitives-Composer.Editor] Displays the composer’s editor. ```tsx ``` The editor’s initial value. The text to display when the editor is empty. Whether the editor is disabled. Whether to focus the editor on mount. The reading direction of the editor and related elements. The components displayed within the editor. | Attribute | Value | | --------------- | --------------------------------------- | | `data-focused` | Present when the component is focused. | | `data-disabled` | Present when the component is disabled. |
###### components [#primitives-Composer.Editor-components] The components displayed within the editor. The component used to display mentions. Defaults to the mention’s `id` prefixed by an @. The component used to display mention suggestions. Defaults to a list of the suggested mentions’ `id`. The component used to display links. Defaults to the link’s `children` property. The component used to display a floating toolbar attached to the selection. ###### Mention [#primitives-Composer.Editor-Mention] The component used to display mentions. ```tsx ( @{mention.id} ), }} /> ``` The mention to display. Whether the mention is selected. ###### MentionSuggestions [#primitives-Composer.Editor-MentionSuggestions] The component used to display mention suggestions. The list of suggested mentions. The currently selected mention’s ID. ```tsx ( ), }} /> ``` ###### Link [#primitives-Composer.Editor-Link] The component used to display links. ```tsx {children}, }} /> ``` The link’s absolute URL. The link’s content. ###### FloatingToolbar [#primitives-Composer.Editor-FloatingToolbar] Displays a floating toolbar attached to the selection within `Composer.Editor`. ```tsx ( Bold Italic ), }} /> ``` ##### Composer.Mention [#primitives-Composer.Mention] Displays mentions within `Composer.Editor`. ```tsx @{mention.id} ``` Replace the rendered element by the one passed as a child. | Attribute | Value | | --------------- | ------------------------------------- | | `data-selected` | Present when the mention is selected. |
##### Composer.Suggestions [#primitives-Composer.Suggestions] Contains suggestions within `Composer.Editor`. ```tsx {/* ... */} ``` Replace the rendered element by the one passed as a child. ##### Composer.SuggestionsList [#primitives-Composer.SuggestionsList] Displays a list of suggestions within `Composer.Editor`. ```tsx {mentions.map((mention) => ( @{mention.id} ))} ``` Replace the rendered element by the one passed as a child. ##### Composer.SuggestionsListItem [#primitives-Composer.SuggestionsListItem] Displays a suggestion within `Composer.SuggestionsList`. ```tsx @{mention.id} ``` The suggestion’s value. Replace the rendered element by the one passed as a child. | Attribute | Value | | --------------- | ---------------------------------- | | `data-selected` | Present when the item is selected. |
##### Composer.Link [#primitives-Composer.Link] Displays links within `Composer.Editor`. ```tsx {children} ``` Replace the rendered element by the one passed as a child. ##### Composer.Submit [#primitives-Composer.Submit] A button to submit the composer. ```tsx Send ``` Replace the rendered element by the one passed as a child. ##### Composer.FloatingToolbar [#primitives-Composer.FloatingToolbar] Displays a floating toolbar attached to the selection within `Composer.Editor`. ```tsx Bold Italic ``` Replace the rendered element by the one passed as a child. ##### Composer.MarkToggle A toggle button which toggles a specific text mark. ```tsx Bold ``` The text mark to toggle. The event handler called when the mark is toggled. Replace the rendered element by the one passed as a child. ##### Composer.AttachFiles [#primitives-Composer.AttachFiles] A button which opens a file picker to create attachments. ```tsx Attach files ``` Replace the rendered element by the one passed as a child. ##### Composer.AttachmentsDropArea [#primitives-Composer.AttachmentsDropArea] A drop area which accepts files to create attachments. ```tsx Drop files here ``` Replace the rendered element by the one passed as a child. #### Comment [#primitives-Comment] Used to render a single comment. ```tsx ``` 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) => (
))} ); } // Render a mention in the comment, e.g. "@Emil Joyce" function Mention({ mention }: CommentBodyMentionProps) { return @{mention.id}; } // Render a link in the comment, e.g. "https://liveblocks.io" function Link({ href, children }: CommentBodyLinkProps) { return {children}; } ``` ##### Comment.Body [#primitives-Comment.Body] Displays a comment body. ```tsx ``` The comment body to display. If not defined, the component will render `null`. The components displayed within the comment body. Replace the rendered element by the one passed as a child. ###### components [#primitives-Comment.Body-components] The components displayed within the comment body. The component used to display mentions. Defaults to the mention’s `id` prefixed by an @. The component used to display links. Defaults to the link’s `children` property. ###### Mention [#primitives-Comment.Body-Mention] The component used to display mentions. ```tsx @{mention.id}, }} /> ``` The mention to display. ###### Link [#primitives-Comment.Body-Link] The component used to display links. ```tsx ( {children} ), }} /> ``` The link’s absolute URL. The link’s content. ##### Comment.Mention [#primitives-Comment.Mention] Displays mentions within `Comment.Body`. ```tsx @{mention.id} ``` Replace the rendered element by the one passed as a child. ##### Comment.Link [#primitives-Comment.Link] Displays links within `Comment.Body`. ```tsx {children} ``` Replace the rendered element by the one passed as a child. #### 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 ``` 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) => (
// +++ // +++
))} ); } ``` The date to display. A function to format the displayed date. Defaults to a relative date formatting function. The `title` attribute’s value or a function to format it. Defaults to an absolute date formatting function. The interval in milliseconds at which the component will re-render. Can be set to `false` to disable re-rendering. The locale used when formatting the date. Defaults to the browser’s locale. Replace the rendered element by the one passed as a child. #### 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 ``` 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. The duration in milliseconds. If provided, `from` and `to` will be ignored. 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. The date at which the duration ends. If `from` is provided without `to`, `Date.now()` will be used. A function to format the displayed date. Defaults to a short duration formatting function. The `title` attribute’s value or a function to format it. Defaults to an longer duration formatting function. 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. The locale used when formatting the duration. Defaults to the browser’s locale. Replace the rendered element by the one passed as a child. #### FileSize [#primitives-FileSize] Displays a formatted file size. ```tsx ``` 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 (
{/* ... */} {comment.attachments.map((attachment) => (
{attachment.name} // +++ // +++
))}
); } ``` The file size to display. A function to format the displayed file size. Defaults to a human-readable file size formatting function. The locale used when formatting the file size. Defaults to the browser’s locale. Replace the rendered element by the one passed as a child. #### 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 ( { addReaction({ threadId: comment.threadId, commentId: comment.id, emoji, }); }} > Loading… No emoji found. ); } ``` 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 ( ); })} ); } ``` ### 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 ( { const thread = createThread({ body, attachments, metadata: {}, }); }} > ); } function Editor() { // +++ const { createMention } = useComposer(); // +++ return ( <> // +++ // +++ ); } ``` ##### 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 ( { const thread = createThread({ body, attachments, metadata: {}, }); }} > // +++ // +++ Attach Files Submit ); } function MyComposerAttachments() { // +++ const { attachments, removeAttachment } = useComposer(); // +++ return (
// +++ {attachments.map((attachment) => (
{attachment.name} ({attachment.status})
))} // +++
); } ``` ##### Values [#useComposer-values] Whether the composer is currently disabled. Whether the editor is currently focused. Whether the editor is currently empty. Whether the composer can currently be submitted. Submit the editor programmatically. Clear the editor programmatically. Select the editor programmatically. Focus the editor programmatically. Blur the editor programmatically. Which text marks are currently active and which aren’t. Toggle a specific text mark. Start creating a mention at the current selection. Insert text at the current selection. Open a file picker programmatically to create attachments. The composer’s current attachments. Remove an attachment by its ID. #### 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) - [`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
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) => ( ))} ); } ``` ##### 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" ( ), $myCustomNotification: (props) => ( ❕} > My custom notification ), }} /> ``` 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"
New notification
, }} /> ``` ##### 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" { // `title` and `message` are correctly typed, as defined in your config const { title, message } = props.inboxNotification.activities[0].data; return ( ❗} > {message} ); }, }} /> ``` 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 ( ❗} > {message} ); } function Notification({ inboxNotification }) { return ( ); } ``` ##### 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 { // Each batched `activityData` is added to the `activities` array const { activities } = props.inboxNotification; return ( ❗} > {activities.map((activity) => (
{activity.data.title}
{activity.data.message}
))}
); }, }} /> ``` ##### Props [#InboxNotification-props] The inbox notification to display. The URL which the inbox notification links to. How to show or hide the actions. Override specific kinds of inbox notifications. Override the component’s strings. Override the component’s components. ###### kinds [#InboxNotification-kinds] Override specific kinds of inbox notifications. The component used to display thread notifications. Defaults to `InboxNotification.Thread`. The component used to display text mention notifications. Defaults to `InboxNotification.TextMention`. The component used to display a custom notification kind. Custom notification kinds must start with a `$`. ###### InboxNotification.Thread [#InboxNotification.Thread] Displays a thread inbox notification kind. ```tsx ( ), }} /> ``` The inbox notification to display. How to show or hide the actions. Whether to show the room name in the title. Whether to show reactions. Whether to show attachments. ###### InboxNotification.TextMention [#InboxNotification.TextMention] Displays a text mention inbox notification kind. ```tsx ( ), }} /> ``` The inbox notification to display. How to show or hide the actions. Whether to show the room name in the title. ###### InboxNotification.Custom [#InboxNotification.Custom] Displays a custom notification kind. ```tsx { const activityData = props.inboxNotification.activities[0].data; return ( User {activityData.file} } aside={} {...props} > {activityData.errorDescription} ); }, }} /> ``` The inbox notification to display. The inbox notification’s title. The inbox notification’s content. How to show or hide the actions. The inbox notification’s aside content. Can be combined with{" "} InboxNotification.Icon or InboxNotification.Avatar{" "} to easily follow default styles. Replace the rendered element by the one passed as a child. ###### InboxNotification.Inspector [#InboxNotification.Inspector] Displays the inbox notification’s data in a JSON code snippet. Useful when debugging notifications in your app. ```tsx ``` The inbox notification to display. How to show or hide the actions. #### InboxNotificationList Displays inbox notifications as a list. Each [`InboxNotification`](#InboxNotification) component will be wrapped in a `li` element. ```tsx ```
InboxNotificationList
##### Props [#InboxNotificationList-props] The inbox notifications to display. ### 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 { setSelectedVersionId(version.id); }} version={version} selected={version.id === selectedVersionId} /> ``` ##### Props [#HistoryVersionSummary-props] The function to call when the version summary is clicked. The version object containing information about the specific version. Whether this version is currently selected. #### HistoryVersionSummaryList Displays a list of version summaries for a document’s history including authors and dates. ```tsx {versions?.map((version) => ( { setSelectedVersionId(version.id); }} key={version.id} version={version} selected={version.id === selectedVersionId} /> ))} ``` ##### Props [#HistoryVersionSummaryList-props] The version summaries to display, typically an array of HistoryVersionSummary components. ## Utilities ### Components [#utilities-components] #### Icon Most icons used in the default components can be exported via ``. They’re stroke-based and designed for use at 20×20 pixels. ```tsx import { Icon } from "@liveblocks/react-ui"; ``` 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 ``` ##### Props [#LiveblocksUiConfig-props] Override the components’ strings. Override the components’ components. The container to render the portal into. 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`. 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`). ### 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; } ``` The border radius scale. `em` values recommended. The spacing scale. `em` values recommended. The accent color. The foreground color used over the accent color. The destructive color. The foreground color used over the destructive color. The main background color. The main foreground color. The line height of main elements (e.g. comment bodies). The size of icons. The stroke weight of icons. The border radius used for avatars. The border radius used for buttons. The duration used for transitioned elements. The easing function used for transitioned elements. The box shadow added to elevated elements. The box shadow added to moderately elevated elements. The box shadow added to tooltips. Affects the lightness of accent colors. `%` value required. Affects the lightness of destructive colors. `%` value required. Affects the lightness of foreground colors. `%` value required. {/* 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 */ } ``` Classes containing colons `:` are internal and may change over time. ### 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 ( {/* ... */} ); } ``` 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 `` component for creating replies to threads: ```tsx ``` --- 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. If you’re using the non-standard [`createRoomContext`](#createRoomContext) function to build your hooks, you must [enable suspense differently](#createRoomContext-Suspense). ### 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() { Loading…}> ; } 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 ( Error}> Loading…}> ); } 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
Loading…
; } // ... } ``` 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
Error
; } if (isLoading) { return
Loading…
; } // ... } ``` ### 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 ( +++ Loading…}> +++ ); } ``` #### 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 (
My title
+++ Loading…}> +++
); } ``` 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 ( {/* children */} ); } ``` ```tsx title="All LiveblocksProvider props" isCollapsable isCollapsed import { LiveblocksProvider } from "@liveblocks/react/suspense"; function App() { return ( { // const response = await fetch("/api/liveblocks-auth", { // method: "POST", // headers: { // Authentication: "", // "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 */} ); } ``` 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). 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). 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). When set, navigating away from the current page is prevented while Liveblocks is still synchronizing local changes. [Learn more](#prevent-users-losing-unsaved-changes). 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). The time before an inactive WebSocket connection is disconnected. This is disabled by default, but setting a number will activate it. [Learn more](#LiveblocksProviderBackgroundKeepAliveTimeout). 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). 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). 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). 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). 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).
How to handle WebSocket messages that are larger than the maximum message size. Can be set to one of these values:
- `"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).
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). Experimental. Stream the initial Storage content over HTTP, instead of waiting for a large initial WebSocket message to be sent from the server.
#### 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 ( {/* children */} ); } ``` #### 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 ( {/* children */} ); } ``` #### 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 ( { // Fetch your authentication endpoint and retrieve your access or ID token // ... return { token: "..." }; }} > {/* children */} ); } ``` `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 ( { const response = await fetch("/api/liveblocks-auth", { method: "POST", headers: { Authentication: "", "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 */} ); } ``` ##### 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 ( {/* children */} ); } ``` 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 ( ... ); } ``` 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 ( {/* children */} ); } ``` 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 ( {/* children */} ); } ``` `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 ( { const usersData = await __getUsersFromDB__(userIds); return usersData.map((userData) => ({ name: userData.name, avatar: userData.avatar.src, })); }} // Other props // ... > {/* children */} ); } ``` 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 ( { // ["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 */} ); } ``` You can also return custom information, for example, a user’s `color`: ```tsx import { LiveblocksProvider } from "@liveblocks/react/suspense"; function App() { return ( { // ["marc@example.com"]; console.log(userIds); return [ { name: "Marc", avatar: "https://example.com/marc.png", // +++ color: "purple", // +++ }, ]; }} // Other props // ... > {/* children */} ); } ``` ##### 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 ( { const documentsData = await __getDocumentsFromDB__(roomIds); return documentsData.map((documentData) => ({ name: documentData.name, // url: documentData.url, })); }} // Other props // ... > {/* children */} ); } ``` 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 ( { const groupsData = await __getGroupsFromDB__(groupIds); return groupsData.map((groupData) => ({ name: groupData.name, avatar: groupData.avatar.src, // description: groupData.description, })); }} // Other props // ... > {/* children */} ); } ``` ##### 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 ( { 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 */} ); } ``` ##### 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 ( { 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 */} ); } ``` 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 ( {/* children */} ); } ``` 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 ( {/* children */} ); } ``` #### 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" ``` 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. Removing the "Powered by Liveblocks" badge on your projects requires a [paid plan](/pricing/). See the [pricing page](/pricing/) for more information. ### createLiveblocksContext 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. 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(); ``` _None_ The [Liveblocks client](/docs/api-reference/liveblocks-client#createClient) instance from the nearest [`LiveblocksProvider`][]. ### 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; } }); ``` A callback function that will be called when a Liveblocks error occurs. The error object contains a message and context with error-specific information. ## 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(); ``` Optional query to filter chats by metadata values or absence of metadata keys. [Learn more](/docs/api-reference/liveblocks-react#useAiChats-query) An array of AI chats created by the current user. Whether the chats are currently being loaded. Any error that occurred while loading the chats. [Learn more](/docs/api-reference/liveblocks-react#useAiChats-error-handling). Whether all available chats have been fetched. [Learn more](/docs/api-reference/liveblocks-react#useAiChats-pagination). A function to fetch more chats. [Learn more](/docs/api-reference/liveblocks-react#useAiChats-pagination). Whether more chats are currently being fetched. [Learn more](/docs/api-reference/liveblocks-react#useAiChats-pagination). Any error that occurred while fetching more chats. [Learn more](/docs/api-reference/liveblocks-react#useAiChats-pagination). ##### 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
Loading...
; } if (error) { return
Error: {error.message}
; } return (
    // +++ {chats.map((chat) => (
  • ))} // +++
// +++ // +++
); } ``` #### 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 (
{chats.map((chat) => ( ))} // +++ {hasFetchedAll ? (
🎉 All chats loaded!
) : ( )} // +++
); } ``` #### 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 (

Error loading AI chats: {error.message}

); } // +++ return (
{chats.map((chat) => ( ))} {fetchMoreError && (

Error loading more AI chats: {fetchMoreError.message}

)}
); } ``` ### 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"); ``` The ID of the AI chat to retrieve information for. The AI chat object containing title, metadata, and other properties. Whether the chat information is currently being loaded. Any error that occurred while loading the chat information. #### 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
{chat.title || "Untitled chat"}
; // +++ } ``` ### useCreateAiChat Returns a function that creates an AI chat. ```tsx import { useCreateAiChat } from "@liveblocks/react/suspense"; const createAiChat = useCreateAiChat(); createAiChat("my-ai-chat"); ``` _None_ 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`. #### 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"); ``` _None_ A function that deletes an AI chat by its ID. ### 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...", // +++ }); ``` Optional. The ID of the AI chat to send messages to. Can also be provided when calling the returned function. Optional configuration object. Optional. The ID of the copilot to use for sending the message. 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`. #### 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"); ``` The ID of the AI chat to retrieve messages from. An array of messages in the AI chat.
```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 } } ] ```
Whether the messages are currently being loaded. Any error that occurred while loading the messages.
### 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"); ``` The ID of the AI chat. The current synchronization status of the chat. When disconnected the AI service is not available. The type of content being generated. The name of the tool being invoked. If no tool is currently being called, returns `undefined`. ### 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 ``` 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 ( <> // +++ // +++ ); } ``` #### 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 ( <> // +++ // +++ ); } ``` 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 ( // +++ // +++ ); } ``` #### 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 ( // +++ // +++ ); } 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 (
// +++ // +++
); } ``` 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 (
// +++ // +++ {threads.map((thread) => ( )}
); } ``` #### 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 (
// +++ // +++ {shapes.map((shape) => ( // ... )}
); } ``` #### Props [#RegisterAiKnowledge-props] A clear description of what this knowledge represents. This helps the AI understand the context and relevance of the provided information. 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. Optional unique identifier for this knowledge source. If provided, subsequent updates with the same ID will replace the previous knowledge. Optional chat ID to scope this tool to a specific chat. If provided, the tool will only be available to that chat. ### 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"; ; ``` {/* 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 ( <> // +++ { const { temperature, condition } = await __getWeather__( args.location ); return { data: { temperature, condition } }; }, render: ({ result }) => ( {result.data ? (
{result.data.temperature}°F - {result.data.condition}
) : null}
), })} /> // +++ ); } ``` #### 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 ( <> { toast(message); return { data: { message }, description: "You sent a toast", }; }, render: () => , })} /> ); } ``` #### 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 ( <> // +++ // +++ ); } ``` #### Props [#RegisterAiTool-props] Unique name for the tool. This is used internally to identify and manage the tool. The tool definition created with [`defineAiTool`](/docs/api-reference/liveblocks-client#defineAiTool). Optional chat ID to scope this tool to a specific chat. If provided, the tool will only be available to that chat. 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`. ## 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 {/* children */}; } ``` 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. 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). 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). Whether the room immediately connects to Liveblocks servers. This value is ignored after the first render. #### 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 ( {/* children */} ); } ``` 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 ( {/* children */} ); } ``` 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 ( // +++ prewarmRoom(roomId)}> // +++ {roomId} ); } ``` `onPointerDown` is slightly quicker than `onClick` because it triggers before the user releases their pointer. ### createRoomContext 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. 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 = {}; // +++ export const { RoomProvider, useMyPresence, // Other hooks // ... } = createRoomContext( 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 = {}; export const { // +++ suspense: { RoomProvider, useMyPresence, // Other suspense hooks // ... }, // +++ } = createRoomContext( 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, // ... }; // 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` ``` Whether the hook should return `null` instead of throwing when used outside of a [`RoomProvider`][] context. The Room instance from the nearest [`RoomProvider`][]. Returns `null` if `allowOutsideRoom` is `true` and the hook is used outside of a room. ### 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(); ``` _None_ `true` if the hook was called inside a [`RoomProvider`][] context, `false` otherwise. #### 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 (
// +++ {isInsideRoom ? : null} // +++
); } function LiveAvatars() { const others = useOthers(); return others.map((other) => ); } ``` 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
// 👤 Just your avatar
// 👤 Just your avatar
``` ### 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(); ``` _None_ The current WebSocket connection status of the room. ### 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(); ``` Optional configuration object. When `true`, prevents quick changes between states by delaying the transition from "synchronizing" to "synchronized" until 1 second has passed after the final change. The current synchronization status of Liveblocks. #### 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
{syncStatus === "synchronized" ? "✅ Saved" : "🔄 Saving"}
; } ``` #### 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 ( ... ); } ``` 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; } }); } ``` 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. ### 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][]. A callback function that is called when a connection loss event occurs. The event can be "lost", "restored", or "failed". ## Presence Try the [Liveblocks DevTools extension](/devtools) to visualize your collaborative experiences as you build them, in realtime. ### 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 }" ``` The current user’s presence data. A function to update the current user’s presence. Accepts a partial presence object and optional history options. #### 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 } ``` _None_ A function to update the current user's presence. Accepts a partial presence object and optional history options. #### 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][]. Optional selector function to extract specific data from the current user. If not provided, returns the entire user object. The current user object or the selected data from the user. Returns `null` if not connected to the room (in non-Suspense version). #### 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
✏️ Full access
; } return
👀 Read-only access
; } ``` 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 (
); } ``` ### 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 } } }, // ] ``` In production-ready apps, you likely want to avoid calling `useOthers` without arguments. Optional selector function to extract specific data from the others array. If not provided, returns the entire others array. Optional equality function to determine if the selected data has changed. Defaults to strict equality comparison. The others array or the selected data from the others array. Returns `null` if not connected to the room (in non-Suspense version). ### 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]) => ( ))} ); ``` A selector function to extract specific data from each user in the others array. Optional equality function to determine if the selected data for a user has changed. Defaults to strict equality comparison. 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). ### 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(); ``` An array of connection IDs for all other users in the room. Returns `null` if not connected to the room (in non-Suspense version). #### 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) => ( ))} ); }); ``` ```tsx file="Cursor.tsx" function Cursor({ connectionId }) { // +++ const { x, y } = useOther(connectionId, (other) => other.presence.cursor); // (4) // +++ return ; } ``` 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`. The connection ID of the specific user to extract data from. A selector function to extract specific data from the user. Optional equality function to determine if the selected data has changed. Defaults to strict equality comparison. 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). ## 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 } }); ``` _None_ A function that broadcasts custom events to other users in the room. ### 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. 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. ## 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; }; } } ``` You can then set an initial value in [`RoomProvider`][]. ```tsx import { LiveList } from "@liveblocks/client"; import { RoomProvider } from "@liveblocks/react/suspense"; function App() { return ( {/* children */} ); } ``` 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 ; } ``` [`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 (
    {animals.map((animal) => (
  • {animal}
  • ))}
); } ``` ### 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; }>; declare global { interface Liveblocks { Storage: { people: LiveMap; }; } } ``` 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 ( {/* children */} ); } ``` Get the [Liveblocks DevTools extension](/devtools) to develop and debug your application as you build it. ### 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][]. 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, ); ``` A selector function to extract specific data from the storage root. The root is an immutable copy of your entire Liveblocks Storage tree. Optional equality function to determine if the selected data has changed. Defaults to strict equality comparison. The selected data from storage. Returns `null` while storage is still loading (in non-Suspense version). ### 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(); ``` _None_ The room's history object containing methods for undo, redo, pause, and resume operations. ### 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(); ``` _None_ A function that undoes the last operation executed by the current client. ### 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(); ``` _None_ A function that redoes the last operation executed by the current client. ### 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 ``` _None_ Whether there are any operations to undo. ### 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 ``` _None_ Whether there are any operations to redo. ### 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 )} // +++ ); } ``` #### 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 (

Error loading threads: {error.message}

); } // +++ return (
{threads.map((thread) => ( ))} {fetchMoreError && (

Error loading more threads: {fetchMoreError.message}

)}
); } ``` #### 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 metadata. ```tsx import { useCreateThread } from "@liveblocks/react/suspense"; const createThread = useCreateThread(); const thread = createThread({ body: {}, attachments: [], metadata: {} }); ``` A function that creates a thread with an initial comment and optional metadata. Returns the optimistic thread object. #### 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. Configuration object. 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. Text to search within comment content. Uses rich text and vector search for relevance. Metadata to filter threads by. Whether to only return comments from threads that have attachments. Whether to only return comments that have attachments. Whether to only return comments that have mentions. An array of matched comments with the parent thread they belong to. Whether the results are currently being loaded. Any error that occurred while searching the threads. #### 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 ( <> setSearch(e.target.value)} /> {isLoading ? (
Loading...
) : ( results.map((result) => ( // +++
{result.content} // +++ )) )} ); } ``` 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("all"); // +++ // +++ const searchThreadResolved = threadResolved === "all" ? undefined : threadResolved === "resolved" ? true : false; // +++ const { results, isLoading, error } = useSearchComments({ query: { text: search, // +++ threadResolved: searchThreadResolved, // +++ }, }); return ( <> setSearch(e.target.value)} /> // +++ // +++ {isLoading ? (
Loading...
) : ( results.map((result) => ( {result.content} )) )} ); } ``` ### 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"); ``` A function that deletes a thread and all its associated comments by ID. #### 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: {} }); ``` A function that edits a thread’s metadata. To delete an existing metadata property, set its value to `null`. #### 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"); ``` A function that marks a thread as resolved. #### 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"); ``` A function that marks a thread as unresolved. #### 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"); ``` A function that marks a thread as read. ### 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. The ID of the thread to get subscription status for. The subscription status of the thread ('subscribed', 'unsubscribed', or 'not_subscribed'). A function to subscribe to the thread. A function to unsubscribe from the thread. The date when the thread was last read, or null if it has been read. ### 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", }); ``` A function that subscribes the current user to a thread for inbox notifications. ### 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", }); ``` A function that unsubscribes the current user from a thread, stopping inbox notifications. #### 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: [], }); ``` A function that adds a comment to a thread. Returns the optimistic comment object. #### 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. ```tsx import { useEditComment } from "@liveblocks/react/suspense"; const editComment = useEditComment(); editComment({ threadId: "th_xxx", commentId: "cm_xxx", body: {}, attachments: [], }); ``` A function that edits a comment’s body and attachments. #### 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}`); } }); ``` ### 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" }); ``` A function that deletes a comment. If it is the last non-deleted comment, the thread also gets deleted. #### 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: "👍" }); ``` A function that adds a reaction to a comment. #### 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: "👍" }); ``` A function that removes a reaction from a comment. #### 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"); ``` The ID of the attachment to get a presigned URL for. The presigned URL for the attachment, or `undefined` if not found or not yet loaded (in non-Suspense version). Whether the URL is currently being loaded. Any error that occurred while loading the URL. ## 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 (
// +++ {inboxNotifications.map((notification) => ( ))} // +++
); } ``` Optional configuration object. Optional query to filter notifications by room ID or kind. [Learn more](/docs/api-reference/liveblocks-react#useInboxNotifications-query). An array of inbox notifications for the current user, or `undefined` if not yet loaded (in non-Suspense version). Whether the notifications are currently being loaded. Any error that occurred while loading the notifications. Whether all available notifications have been fetched. [Learn more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination). A function to fetch more notifications. [Learn more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination). Whether more notifications are currently being fetched. [Learn more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination). Any error that occurred while fetching more notifications. [Learn more](/docs/api-reference/liveblocks-react#useInboxNotifications-pagination). #### 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 (
{inboxNotifications.map((notification) => ( ))} // +++ {hasFetchedAll ? (
🎉 You're all caught up!
) : ( )} // +++
); } ``` #### 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 (

Error loading inbox notifications: {error.message}

); } // +++ return (
{inboxNotifications.map((notification) => ( ))} {fetchMoreError && (

Error loading more inbox notifications: {fetchMoreError.message}

)}
); } ``` #### 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(); ``` Optional configuration object. Optional query to filter notifications count by room ID or kind. The number of unread inbox notifications for the current user, or `undefined` if not yet loaded (in non-Suspense version). Whether the number of unread inbox notifications is currently being loaded. Any error that occurred while loading the number of unread inbox notifications. #### 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"); ``` A function that marks an inbox notification as read for the current user. #### 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(); ``` A function that marks all of the current user's inbox notifications as read. #### 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"); ``` A function that deletes an inbox notification for the current user. #### 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(); ``` A function that deletes all of the current user’s inbox notifications. #### 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. 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. The ID of the inbox notification to get the associated thread for. Must be a "thread" type notification. The thread associated with the inbox notification, or null if not found. ### 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. The current subscription settings for the room. A function to update the subscription settings. ### 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). A function that updates the user's notification settings for the current room. ##### 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] Notification settings is currently in 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 `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", }); ``` The current notification settings for the user, or null if no settings are configured. A function to update the notification settings. Whether the notification settings are currently being loaded. Any error that occurred while loading the notification settings. #### 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 (

Error loading notification settings: {error.message}

); } ``` ### useUpdateNotificationSettings [@badge=LiveblocksProvider] Notification settings is currently in beta. 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, }, }); ``` A function that updates the user's notification settings for the current project. ## 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(); ``` An array of history versions for the room. ## 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"); ``` The ID of the user to get information for. The user information, or `undefined` if not found or not yet loaded (in non-Suspense version). Whether the user information is currently being loaded. Any error that occurred while loading the user information. ### 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"); ``` The ID of the room to get information for. The room information, or `undefined` if not found or not yet loaded (in non-Suspense version). Whether the room information is currently being loaded. Any error that occurred while loading the room information. ### 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"); ``` The ID of the group to get information for. The group information, or `undefined` if not found or not yet loaded (in non-Suspense version). Whether the group information is currently being loaded. Any error that occurred while loading the group information. ### 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, ... ``` The URL to get metadata for. The metadata for the URL, or `undefined` if not yet loaded (in non-Suspense version). Whether the metadata is currently loading. Any error that occurred while loading the metadata. ## 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; }; 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, // ... }; // 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(); ``` The connection ID of the User. It is unique and increments with every new connection. The ID of the User that has been set in the authentication endpoint. Useful to get additional information about the connected user. Additional user information that has been set in the authentication endpoint. The user’s Presence data. `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). `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). ## 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); ``` 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. 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
✅ `(root) => root.animals` is stable
Liveblocks guarantees this. All nodes in the Storage tree are stable references as long as their contents don’t change.
️️⚠️ `(root) => root.animals.map(...)` is not stable
Because `.map()` creates a new array instance every time. You’ll need to use [`shallow`][] here.
✅ `(root) => root.animals.map(...).join(", ")` is stable
Because `.join()` ultimately returns a string and all primitive values are always stable.
#### 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: {}, }) ), }); ``` The Liveblocks client instance created with `createClient()`. Optional mapping to synchronize Redux state with Liveblocks presence. Optional mapping to synchronize Redux state with Liveblocks storage. ### 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")); ``` The ID of the room to enter. ### 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()); ``` _None_ ## 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(); ``` The [`LiveblocksYjsProvider`][] for the room. The provider is automatically cleaned up and destroyed when necessary. Fetch your [`Y.Doc`][] with [`LiveblocksYjsProvider.getYDoc`](#LiveblocksYjsProvider.getYDoc). The Liveblocks room, retrieved with [`useRoom`][] or [`client.enterRoom`][]. This option will load subdocs automatically. This option enables Yjs permanent user data class used by some libraries for tracking changes by author. Experimental. Enable offline support using IndexedDB. This means that after the first load, documents will be stored locally and load instantly. Experimental. Use V2 encoding. ## 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. We generally recommend getting your Liveblocks Yjs provider with [`getYjsProviderForRoom`][] as it overcomes problems caused when dynamically switching between rooms. 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 // ... }); ``` The Liveblocks room, retrieved with [`useRoom`][] or [`client.enterRoom`][]. The [`Y.Doc`][] for the document. This option will load subdocs automatically. This option enables Yjs permanent user data class used by some libraries for tracking changes by author. Experimental. Enable offline support using IndexedDB. This means the after the first load, documents will be stored locally and load instantly. Experimental. Use V2 encoding. ### 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 = 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 = 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 = 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: {}, } ) ); ``` The Zustand state creator function. Configuration object for the Liveblocks middleware. The Liveblocks client instance created with `createClient()`. Optional mapping to synchronize Zustand state with Liveblocks presence. Optional mapping to synchronize Zustand state with Liveblocks storage. ### 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"); ``` The ID of the room to enter. 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(); ``` _None_ ### 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] Try the [Liveblocks DevTools extension](/devtools) to visualize your collaborative experiences as you build them, in realtime. ### 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 `
There are {others.length} other user(s) online
``` Next: set up authentication 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. ## 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 --- 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. 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). ## Quickstart Install Liveblocks Every package should use the same version. ```bash npm install @liveblocks/client ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up the Liveblocks client 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}}", }); ``` Join a Liveblocks room 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"); ``` Use the Liveblocks methods 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" ``` Next: set up authentication 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. ## 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 --- meta: title: "Get started with Liveblocks, CodeMirror, Yjs, and JavaScript" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, CodeMirror, Yjs, 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 Install Liveblocks, Yjs, and CodeMirror Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next ``` Initialize the `liveblocks.config.ts` file 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 ``` Create an HTML element ```html
```
Set up your collaborative CodeMirror code editor ```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, }); ``` Next: set up authentication 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.
## 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 Liveblocks, CodeMirror, Yjs, and React" parentTitle: "Quickstart" description: "Learn how to get started with CodeMirror, Yjs, and Liveblocks." --- 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 Install Liveblocks, Yjs, and CodeMirror Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up the Liveblocks client 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 ( {/* ... */} ); } ``` Join a Liveblocks room 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 ( Loading…}> ); } ``` Set up the collaborative CodeMirror editor 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(); 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
; } ``` Next: set up authentication 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. ## 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 --- meta: title: "Get started with Liveblocks, CodeMirror, Yjs, and Svelte" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, CodeMirror, Yjs, 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 Install Liveblocks, Yjs, and CodeMirror Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up your collaborative CodeMirror code editor ```html file="Editor.svelte"
```
Next: set up authentication 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.
## 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 Liveblocks, CodeMirror, Yjs, and Vue.js" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, CodeMirror, Yjs, 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 Install Liveblocks, Yjs, and CodeMirror Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up your collaborative CodeMirror code editor ```html file="Editor.vue" ``` Next: set up authentication 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. ## 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 Liveblocks, Monaco, Yjs, and JavaScript" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Monaco, Yjs, 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 Install Liveblocks, Yjs, and Monaco Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs monaco-editor y-monaco ``` Initialize the `liveblocks.config.ts` file 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 ``` Create an HTML element ```html
```
Set up your collaborative Monaco code editor ```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 ); ``` Next: set up authentication 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.
## 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 Liveblocks, Yjs, Monaco, and React" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Yjs, Monaco, 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 Install Liveblocks, Yjs, and Monaco Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs @monaco-editor/react y-monaco y-protocols ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up the Liveblocks client 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 ( // +++ {/* ... */} // +++ ); } ``` Join a Liveblocks room 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 ( Loading…
}> ); } ```
Set up the collaborative Monaco code editor 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(); 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 ( ); } ``` Next: set up authentication 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.
## 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 --- meta: title: "Get started with Liveblocks, Monaco, Yjs, and Svelte" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Monaco, Yjs, 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 Install Liveblocks, Yjs, and Monaco Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs monaco-editor y-monaco ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up your collaborative Monaco code editor ```html file="Editor.svelte"
```
Next: set up authentication 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.
## 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 Liveblocks, Monaco, Yjs, and Vue.js" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Monaco, Yjs, 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 Install Liveblocks, Yjs, and Monaco Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs monaco-editor y-monaco ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up your collaborative Monaco code editor ```html file="Editor.vue" ``` Next: set up authentication 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. ## 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 Liveblocks, Quill, Yjs, and JavaScript" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Quill, Yjs, 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 Install Liveblocks, Yjs, and Quill Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs quill quill-cursors y-quill ``` Initialize the `liveblocks.config.ts` file 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 ``` Create an HTML element ```html
```
Set up your collaborative Quill text editor ```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); ``` Next: set up authentication 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.
## 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 Liveblocks, Yjs, Quill, and React" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Yjs, Quill, 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 Install Liveblocks, Yjs, and Quill Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs quill quill-cursors react-quill y-quill ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up the Liveblocks client 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 ( {/* ... */} ); } ``` Join a Liveblocks room 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 ( Loading…}> ); } ``` Set up the collaborative Quill text editor 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 ; } type EditorProps = { yText: Y.Text; provider: any; }; function QuillEditor({ yText, provider }: EditorProps) { const reactQuillRef = useRef(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 ( ); } ``` Next: set up authentication 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. ## 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 --- meta: title: "Get started with Liveblocks, Quill, Yjs, and Svelte" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Quill, Yjs, 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 Install Liveblocks, Yjs, and Quill Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs quill quill-cursors y-quill ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up your collaborative Quill text editor ```html file="Editor.svelte"
```
Next: set up authentication 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.
## 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 Liveblocks, Quill, Yjs, and Vue.js" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Quill, Yjs, 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 Install Liveblocks, Yjs, and Quill Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs quill quill-cursors y-quill ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up your collaborative Quill text editor ```html file="Editor.vue" ``` Next: set up authentication 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. ## 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 Liveblocks, Slate, Yjs, and React" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Slate, Yjs, 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 Install Liveblocks, Yjs, and Slate Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs slate slate-react @slate-yjs/core ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up the Liveblocks client 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 ( {/* ... */} ); } ``` Join a Liveblocks room 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 ( Loading…}> ); } ``` Set up the collaborative Slate text editor 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
Loading…
; } return ; } 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 (
); } ``` 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; } ```
Next: set up authentication 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.
## 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 --- meta: title: "Get started with Liveblocks, Tiptap, Yjs, and JavaScript" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Tiptap, Yjs, 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 Install Liveblocks, Yjs, and Tiptap Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor y-prosemirror ``` Initialize the `liveblocks.config.ts` file 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 ``` Create an HTML element ```html
```
Set up your collaborative Tiptap text editor ```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, }), ], }); ``` Next: set up authentication 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.
## 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 Liveblocks, Tiptap, Yjs, and Svelte" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Tiptap, Yjs, 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 Install Liveblocks, Yjs, and Tiptap Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor y-prosemirror ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up your collaborative Tiptap text editor ```html file="Editor.svelte"
```
Next: set up authentication 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.
## 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 Liveblocks, Tiptap, Yjs, and Vue.js" parentTitle: "Quickstart" description: "Learn how to get started with Liveblocks, Tiptap, Yjs, 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 Install Liveblocks, Yjs, and Tiptap Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/yjs yjs @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor y-prosemirror ``` Initialize the `liveblocks.config.ts` file 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 ``` Set up your collaborative Tiptap text editor ```html file="Editor.vue" ``` Next: set up authentication 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. ## 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 Install Liveblocks Every Liveblocks package should use the same version. ```bash npm install @liveblocks/client @liveblocks/zustand ``` Initialize the `liveblocks.config.ts` file 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 ``` Connect your Zustand store to Liveblocks 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>()( liveblocks( (set) => ({ // Your state and actions will go here }), { client } ) ); export default useStore; ``` Join a Liveblocks room 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 ; }; export default App; ``` Use the Liveblocks data from the store 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
There are {userCount} other user(s) online
; } ```
Next: set up authentication 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.
## 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 --- meta: title: "Documentation" description: "Explore the documentation to learn how to build collaborative experiences with Liveblocks." showTitle: false --- ## Ready-made features ## SDKs and packages } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ## Examples ## Community } openInNewWindow /> } openInNewWindow /> } openInNewWindow /> --- 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. --- 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.
Create a Liveblocks account
### 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." --- 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. 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.
Delete your Liveblocks account
## 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." --- 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. _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. 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. 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.
Liveblocks team security settings
### 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.
Liveblocks team security settings
--- 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**.
Liveblocks team members
Enter the email address of the person you’d like to add, select their role, and press the **Invite** button.
Invite team member to Liveblocks
## 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." --- 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. _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.
Liveblocks team setup MFA
## 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." --- 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. 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.
Liveblocks team security settings
## 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.
Liveblocks team security settings
## 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.
Liveblocks team security settings
--- 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.
Liveblocks analytics
You can adjust the period through the timeframe selector by picking between 1 hour, 1 day, 1 week, and 1 month.
Liveblocks analytics timeframe selector
--- 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. Visit our [trust center](https://liveblocks.safebase.us/) to download security and compliance reports, such as SOC 2 Type II and HIPAA. ## 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. --- 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, and Enterprise. Each plan has its own set of limits. | | Free | Pro | Enterprise | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Monthly active rooms | | included
| Up to | | Monthly active users | | | | | Projects | | | Up to | | Team members | | included
| Up to | | Simultaneous connections per room | | | | | Simultaneous connections per project | | | | | Monthly anonymous connections Public key only recommended for prototyping. Set up authentication. | | | | | Realtime data storage 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. | | included
| Up to | | Realtime data stored per room | | | | | File storage 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. | | included
| | | Max file upload size | | | Up to | | Comments stored | | | | | Monthly collaboration notifications Notifications automatically triggered from collaborative features like comment thread updates and @ mentions. | | | | | Monthly custom notifications Non-collaborative notifications that can be triggered from your own code. | | included
| Up to | | Webhook event frequency The frequency at which `yDocUpdated` (Yjs) and `storageUpdated` (Liveblocks Storage) events are sent to your webhook endpoint. Learn more | | | Down to | | Version history APIs, hooks, and pre-built components to access the previous versions of the realtime data stored in rooms. | | Unlimited with Version History Plus add‑on | | | Event log retention | | | Up to | ## Other limits | Item | Limit | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | `roomId` The unique identifier for a room. What's a room? | 128 characters | | `userId` The unique identifier for a user. Each `userId` must be used to represent a single user. | 128 characters | | `userInfo` User information sent from the authentication endpoint. | 1024 characters once serialized to JSON | | Broadcast event message Messages sent via the broadcast API for real-time communication between clients. | 1 MB | | `LiveObject` A realtime data structure that stores key-value pairs. Learn more | 128 kB when totalling the size of the keys and values | | `LiveMap` A realtime data structure that stores key-value pairs in a map. Learn more | Unlimited, so long as each individual value does not exceed 128 kB | | `LiveList` A realtime data structure that stores an ordered list of items. Learn more | 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 the `roomId`. ### 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 A collaborative SaaS product An AI chatbot A multiplayer game An interactive multiplayer landing page ## Never fair use Using a single `userId` for multiple users Abusing free REST API endpoints Load testing without authorization Not using Liveblocks for collaboration Sharing logins or using group accounts for the admin dashboard ## 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 | | --------------------------------- | -------------------------------------------- | ------------------------------------------- | | Average connections per month | Up to | Up to | | Total active room hours per month | Up to | Up to | Usage is measured at the team workspace level. For teams on the Pro plan, you can pay for [additional usage](/docs/platform/limits) as you go. ### Additional usage For members of our Pro plan, 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 | | -------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------ | | Monthly active rooms | | | | Team members | | | | Realtime data storage | | | | File storage | | | | Monthly custom notifications | | | 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 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.
Start creating a project from the Liveblocks dashboard
### 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.
Creating a project from the Liveblocks dashboard
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. A project’s environment cannot be changed later. ### 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) A project’s location cannot be changed later. ## 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.
Liveblocks project overview
### 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.
Liveblocks project rooms
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.
Liveblocks project rooms detail
### Project API keys [#project-api-keys] The **API keys** tab enables you to manage, view, and roll your **public** and **secret** API keys.
Liveblocks project API keys
### 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. From the dashboard overview, navigate to the project you’d like to add webhooks to. Click on the webhooks tab from the left-hand menu. Click the **“Create endpoint…”** button. Enter the URL of the endpoint you would like to use. Configure with your own endpoint or generate a Svix playground link by clicking on **"use Svix play"**. Select the events you would like to subscribe to. Click **“Create endpoint”**.
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. Select the endpoint you would like to edit from the list of webhooks in the dashboard. Select **“Edit endpoint…”** from the top right dropdown. Update event selections and click **“Save changes”**.
### 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…”**.
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. Install the package ```bash npm install @liveblocks/node ``` Set up the webhook handler 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_..."); ``` Verify an event request 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, }); ``` Note that some frameworks parse request bodies into objects, so you may need to use `rawBody: JSON.stringify(req.body)` instead. 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. Full example 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(); } ``` ### 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.
How to manually verify webhook events in Node.js Construct the signed content 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}`; ``` Generate the signature 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"); ``` Validate the signature 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. Verify the timestamp 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. We recommend implementing a constant-time string comparison method when comparing signatures to prevent timing attacks.
### 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.
How to manually verify webhook events in Elixir Construct the signed content 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. Generate the signature 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" # Extract and decode the base64 part of the secret secret_bytes = secret |> String.split("_") |> Enum.at(1) |> Base.decode64!() # Compute the signature signature = :crypto.mac(:hmac, :sha256, secret_bytes, signed_content) |> Base.encode64() ``` Validate the signature The signature you just generated should match one of the signatures from the `webhook-signature` header. That header contains space-separated values like: ``` v1,abc123= v1,def456= v2,ghi789= ``` You should extract just the Base64-encoded signature (the part after the comma): ```elixir signature_header = get_req_header(conn, "webhook-signature") |> List.first() webhook_signatures = signature_header |> String.split(" ") |> Enum.map(fn entry -> entry |> String.split(",") |> Enum.at(1) end) ``` Then check if your generated signature is in the list: ```elixir if signature in webhook_signatures do # Valid signature end ``` Verify the timestamp Liveblocks includes a `webhook-timestamp` header to help prevent replay attacks. You should check that the timestamp is within a reasonable window (e.g., 5 minutes): ```elixir tolerance = 5 * 60 now = :os.system_time(:second) case Integer.parse(webhook_timestamp) do {ts, _} when abs(now - ts) <= tolerance -> true _ -> false end ``` Full example Here’s the full code, as detailed so far. ```elixir title="Plug example" isCollapsed isCollapsable defmodule App.Plugs.RequestValidator do require Logger def init(_), do: [] def call(%Plug.Conn{request_path: "/webhook/liveblocks"} = conn, _opts) do webhook_secret = Application.get_env(:api, :liveblocks)[:webhook_secret] webhook_id = conn |> Plug.Conn.get_req_header("webhook-id") |> List.first() webhook_timestamp = conn |> Plug.Conn.get_req_header("webhook-timestamp") |> List.first() webhook_signatures = conn |> Plug.Conn.get_req_header("webhook-signature") |> List.first() |> String.split(" ") |> Enum.map(&(&1 |> String.split(",") |> Enum.at(1))) {:ok, body, conn} = Plug.Conn.read_body(conn) signed_content = "#{webhook_id}.#{webhook_timestamp}.#{body}" secret_bytes = webhook_secret |> String.split("_") |> Enum.at(1) |> Base.decode64!() signature = :hmac |> :crypto.mac(:sha256, secret_bytes, signed_content) |> Base.encode64() if signature in webhook_signatures do if liveblocks_timestamp_valid?(webhook_timestamp) do params = Jason.decode!(body) conn |> Plug.Conn.assign(:request_validated, true) |> struct(%{body_params: params}) else Logger.warning("[RequestValidator] Liveblocks webhook timestamp is not valid", %{ timestamp: webhook_timestamp }) conn |> Plug.Conn.put_status(:unauthorized) |> Phoenix.Controller.text("Incorrect timestamp") |> Plug.Conn.halt() end else Logger.warning("[RequestValidator] Liveblocks webhook signatures don't match", %{ header_signature: webhook_signatures, calculated_signature: signature }) conn |> Plug.Conn.put_status(:unauthorized) |> Phoenix.Controller.text("Incorrect signature") |> Plug.Conn.halt() end end def call(conn, _), do: conn defp liveblocks_timestamp_valid?(timestamp) do allowed_age = Application.get_env(:api, :liveblocks)[:webhook_timestamp_tolerance] || 5 * 60 now = :os.system_time(:second) case Integer.parse(timestamp) do {ts, _} -> age = abs(now - ts) age <= allowed_age _ -> false end end end ``` Add to your endpoint module Finally, to use the validator to your endpoint module, place it before `Plug.parsers`. ```elixir plug(App.Plugs.RequestValidator) plug(Plug.Parsers, ...) ```
## Testing locally [#testing-locally] Running webhooks locally can be difficult, but there are several tools that allow you to temporarily host your localhost server online. ### Using svix-cli The [`svix-cli`](https://github.com/svix/svix-webhooks/tree/main/svix-cli) provides a `listen` command that creates a publicly accessible URL for testing webhooks without requiring any account setup or network configuration changes. If your project is running on `localhost:3000`, you can run the following command to generate a temporary URL: ```bash svix listen http://localhost:3000/api/liveblocks-webhook ``` This will output a unique URL that forwards all POST requests to your local endpoint: ``` Webhook Relay is now listening at: https://play.svix.com/in/c_tSdQhb4Q5PTF5m2juiWu8qFREqE/ All requests on this endpoint will be forwarded to your local URL: http://localhost:3000/api/liveblocks-webhook View logs and debug information at: https://play.svix.com/view/c_tSdQhb4Q5PTF5m2juiWu8qFREqE/ ``` The generated URL can be placed directly into the Liveblocks webhooks dashboard for testing. This approach is particularly useful in enterprise environments where tools like `localtunnel` or `ngrok` may be blocked by security policies. ### Using localtunnel or ngrok Alternatively, you can use tools such as [`localtunnel`](https://www.npmjs.com/package/localtunnel) or [`ngrok`](https://www.npmjs.com/package/ngrok) which also allow you to temporarily host your localhost server online. If your project is running on `localhost:3000`, you can run the following command to generate a temporary URL that’s available while your localhost server is running: ```bash npx localtunnel --port 3000 ``` If you visit the page `localtunnel` links you to, and correctly input your IP address, the URL it generates can be placed into the Liveblocks webhooks dashboard for quick testing. For a full step-by-step guide on testing with `localtunnel` and `ngrok`, read the guide on [how to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost#Use-your-webhook-event). ## Source IP Addresses [#source-ips] In case your webhook receiving endpoint is behind a firewall or NAT, you may need to allow traffic from [Svix's IP addresses](https://docs.svix.com/receiving/source-ips). ``` 44.228.126.217 50.112.21.217 52.24.126.164 54.148.139.208 2600:1f24:64:8000::/52 ``` ## Liveblocks events An event occurs when a change is made to Liveblocks data. Each endpoint you provide in the webhooks dashboard listens to all events by default but can be easily configured to only listen to a subset by updating the Message Filtering section. The Event Catalog in the webhooks dashboard provides a list of events available for subscription, along with their schema. Events available for use include: - `StorageUpdated` - `UserEntered/UserLeft` - `RoomCreated/RoomDeleted` - `YDocUpdated` - `CommentCreated/CommentEdited/CommentDeleted` - `CommentReactionAdded/CommentReactionRemoved` - `ThreadCreated/ThreadDeleted/ThreadMetadataUpdated` - `Notification` More events will come later, such as: - `MaxConnectionsReached` #### UserEnteredEvent When a user connects to a room, an event is triggered, indicating that the user has entered. The `numActiveUsers` field shows the number of users in the room after the user has joined. This event is not throttled. ```ts // Schema type UserEnteredEvent = { type: "userEntered"; data: { projectId: string; roomId: string; connectionId: number; userId: string | null; userInfo: Record | null; enteredAt: string; numActiveUsers: number; }; }; // Example const userEnteredEvent = { type: "userEntered", data: { projectId: "my-project-id", roomId: "my-room-id", connectionId: 4, userId: "a-user-id", userInfo: null, enteredAt: "2021-10-06T01:45:56.558Z", numActiveUsers: 8, }, }; ``` #### UserLeftEvent A user leaves a room when they disconnect from a room, which is when this event is triggered. The `numActiveUsers` field represents the number of users in the room after the user has left. This event, like `UserEntered`, is not throttled. ```ts // Schema type UserLeftEvent = { type: "userLeft"; data: { projectId: string; roomId: string; connectionId: number; userId: string | null; userInfo: Record | null; leftAt: string; numActiveUsers: number; }; }; // Example const userLeftEvent = { type: "userLeft", data: { projectId: "my-project-id", roomId: "my-room-id", connectionId: 4, userId: "a-user-id", userInfo: { name: "John Doe", }, leftAt: "2021-10-06T01:45:56.558Z", numActiveUsers: 7, }, }; ``` #### StorageUpdatedEvent Storage is updated when a user writes to storage. This event is throttled at 60 seconds and, as such, may not be triggered for every write. For example, if a user writes to storage at 1:00 pm sharp, the `StorageUpdatedEvent` event will be triggered shortly after. If the user writes to Storage again at 1:00 pm and 2 seconds, the `StorageUpdatedEvent` event will be triggered 60 seconds after the first event was sent, around 1:01 pm. On [Enterprise plans](/pricing) we can increase the throttle rate. ```ts // Schema type StorageUpdatedEvent = { type: "storageUpdated"; data: { roomId: string; projectId: string; updatedAt: string; }; }; // Example const storageUpdatedEvent = { type: "storageUpdated", data: { projectId: "my-project-id", roomId: "my-room-id", updatedAt: "2021-10-06T01:45:56.558Z", // 👈 time of the last write }, }; ``` #### RoomCreatedEvent An event is triggered when a room is created. This event is not throttled. There are two ways for rooms to be created: - By calling the [create room API](/docs/api-reference/rest-api-endpoints#post-rooms) - When a user connects to a room that does not exist ```ts // Schema type RoomCreatedEvent = { type: "roomCreated"; data: { projectId: string; roomId: string; createdAt: string; }; }; // Example const roomCreatedEvent = { type: "roomCreated", data: { projectId: "my-project-id", roomId: "my-room-id", createdAt: "2021-10-06T01:45:56.558Z", }, }; ``` #### RoomDeletedEvent An event is triggered when a room is deleted. This event is not throttled. ```ts // Schema type RoomDeletedEvent = { type: "roomDeleted"; data: { projectId: string; roomId: string; deletedAt: string; }; }; // Example const roomDeletedEvent = { type: "roomDeleted", data: { projectId: "my-project-id", roomId: "my-room-id", deletedAt: "2021-10-06T01:45:56.558Z", }, }; ``` #### YDocUpdatedEvent Yjs document is updated when a user makes a change to a Yjs doc connected to a room. This event is throttled at sixty seconds and, as such, may not be triggered for every write. For example, if a user updates a Yjs document at 1:00 pm sharp, the `YDocUpdatedEvent` event will be triggered shortly after. If the user writes to the Yjs document again at 1:00 pm and 2 seconds, the `YDocUpdatedEvent` event will be triggered 60 seconds after the first event was sent, around 1:01 pm On [Enterprise plans](/pricing) we can increase the throttle rate. ```ts // Schema type YDocUpdatedEvent = { type: "ydocUpdated"; data: { projectId: string; roomId: string; updatedAt: string; }; }; // Example const ydocUpdatedEvent = { type: "ydocUpdated", data: { projectId: "my-project-id", roomId: "my-room-id", updatedAt: "2013-06-26T19:10:19Z", }, }; ``` #### CommentCreatedEvent An event is triggered when a comment is created. This event is not throttled. ```ts // Schema type CommentCreatedEvent = { type: "commentCreated"; data: { projectId: string; roomId: string; threadId: string; commentId: string; createdAt: string; createdBy: string; }; }; // Example const commentCreatedEvent = { type: "commentCreated", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", commentId: "my-comment-id", createdAt: "2021-10-06T01:45:56.558Z", createdBy: "my-user-id", }, }; ``` #### CommentEditedEvent An event is triggered when a comment is edited. This event is not throttled. ```ts // Schema type CommentEditedEvent = { type: "commentEdited"; data: { projectId: string; roomId: string; threadId: string; commentId: string; editedAt: string; }; }; // Example const commentEditedEvent = { type: "commentEdited", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", commentId: "my-comment-id", editedAt: "2021-10-06T01:45:56.558Z", }, }; ``` #### CommentDeletedEvent An event is triggered when a comment is deleted. This event is not throttled. ```ts // Schema type CommentDeletedEvent = { type: "commentDeleted"; data: { projectId: string; roomId: string; threadId: string; commentId: string; deletedAt: string; }; }; // Example const commentDeletedEvent = { type: "commentDeleted", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", commentId: "my-comment-id", deletedAt: "2021-10-06T01:45:56.558Z", }, }; ``` #### CommentReactionAddedEvent An event is triggered when a reaction is added to a comment. This event is not throttled. ```ts // Schema type CommentReactionAddedEvent = { type: "commentReactionAdded"; data: { projectId: string; roomId: string; threadId: string; commentId: string; emoji: string; addedAt: string; addedBy: string; }; }; // Example const commentReactionAddedEvent = { type: "commentReactionAdded", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", commentId: "my-comment-id", emoji: "👍", addedAt: "2021-10-06T01:45:56.558Z", addedBy: "my-user-id", }, }; ``` #### CommentReactionRemovedEvent An event is triggered when a reaction is removed from a comment. This event is not throttled. ```ts // Schema type CommentReactionRemovedEvent = { type: "commentReactionRemoved"; data: { projectId: string; roomId: string; threadId: string; commentId: string; emoji: string; removedAt: string; removedBy: string; }; }; // Example const commentReactionRemovedEvent = { type: "commentReactionRemoved", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", commentId: "my-comment-id", emoji: "👍", removedAt: "2021-10-06T01:45:56.558Z", removedBy: "my-user-id", }, }; ``` #### ThreadCreatedEvent An event is triggered when a thread is created. This event is not throttled. ```ts // Schema type ThreadCreatedEvent = { type: "threadCreated"; data: { projectId: string; roomId: string; threadId: string; createdAt: string; createdBy: string; }; }; // Example const threadCreatedEvent = { type: "threadCreated", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", createdAt: "2021-10-06T01:45:56.558Z", createdBy: "my-user-id", }, }; ``` #### ThreadDeletedEvent An event is triggered when a thread is deleted. This event is not throttled. A thread is deleted when all comments in the thread are deleted or when the thread is manually deleted. ```ts // Schema type ThreadDeletedEvent = { type: "threadDeleted"; data: { projectId: string; roomId: string; threadId: string; deletedAt: string; }; }; // Example const threadDeletedEvent = { type: "threadDeleted", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", deletedAt: "2021-10-06T01:45:56.558Z", }, }; ``` #### ThreadMetadataUpdatedEvent An event is triggered when a thread metadata is updated. This event is not throttled. ```ts // Schema type ThreadMetadataUpdatedEvent = { type: "threadMetadataUpdated"; data: { projectId: string; roomId: string; threadId: string; updatedAt: string; updatedBy: string; }; }; // Example const threadMetadataUpdatedEvent = { type: "threadMetadataUpdated", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", updatedAt: "2021-10-06T01:45:56.558Z", updatedBy: "my-user-id", }, }; ``` #### ThreadMarkedAsResolvedEvent An event is triggered when a thread is marked as resolved. This event is not throttled. ```ts // Schema type ThreadMarkedAsResolvedEvent = { type: "threadMarkedAsResolved"; data: { projectId: string; roomId: string; threadId: string; updatedAt: string; updatedBy: string; }; }; // Example const threadMarkedAsResolvedEvent = { type: "threadMarkedAsResolved", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", updatedAt: "2021-10-06T01:45:56.558Z", updatedBy: "my-user-id", }, }; ``` #### ThreadMarkedAsUnresolvedEvent An event is triggered when a thread is marked as unresolved. This event is not throttled. ```ts // Schema type ThreadMarkedAsUnresolvedEvent = { type: "threadMarkedAsUnresolved"; data: { projectId: string; roomId: string; threadId: string; updatedAt: string; updatedBy: string; }; }; // Example const threadMarkedAsUnresolvedEvent = { type: "threadMarkedAsUnresolved", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", updatedAt: "2021-10-06T01:45:56.558Z", updatedBy: "my-user-id", }, }; ``` #### NotificationEvent Notification events are designed to help you create notification emails for your users. By default, they’re triggered 30 minutes after an activity occurs, but this number can be modified in your [dashboard](/dashboard) inside a project’s settings. This webhook event is triggered by both Liveblocks and custom notification `kinds`, as detailed below. ##### Thread notification When using [Comments](/docs/ready-made-features/comments), an event is triggered 30 minutes after a user has been mentioned or replied to in a thread, and has not seen the thread. It will also be triggered if the user has subscribed to the thread and has not seen the thread. The event won’t be triggered if the user has seen the thread or unsubscribed from the room’s thread notifications. This is the Liveblocks `thread` notification kind. ```ts // Schema type ThreadNotificationEvent = { type: "notification"; data: { channel: "email"; kind: "thread"; projectId: string; roomId: string; userId: string; threadId: string; inboxNotificationId: string; // Date representing the time when the webhook event was created. createdAt: string; // Date representing the time when the notification itself was created. triggeredAt: string; }; }; // Example const threadNotificationEvent = { type: "notification", data: { channel: "email", kind: "thread", projectId: "my-project-id", roomId: "my-room-id", userId: "my-user-id", threadId: "my-thread-id", inboxNotificationId: "my-inbox-notification-id", createdAt: "2021-10-06T01:45:56.558Z", triggeredAt: "2021-10-06T01:50:56.558Z", }, }; ``` If you want to easily identify this event in your code then you can use the type guard [`isThreadNotificationEvent`](/docs/api-reference/liveblocks-node#isThreadNotificationEvent). ##### TextMention notification When using [Text editor](/docs/ready-made-features/text-editor), an event is triggered 30 minutes after a user has been mentioned in a text and has not seen the text mention. This is the Liveblocks `textMention` notification kind. ```ts // Schema type TextMentionNotificationEvent = { type: "notification"; data: { channel: "email"; kind: "textMention"; projectId: string; roomId: string; userId: string; mentionId: string; inboxNotificationId: string; // Date representing the time when the webhook event was created. createdAt: string; // Date representing the time when the notification itself was created. triggeredAt: string; }; }; // Example const textMentionNotificationEvent = { type: "notification", data: { channel: "email", kind: "textMention", projectId: "my-project-id", roomId: "my-room-id", userId: "my-user-id", mentionId: "my-mention-id", inboxNotificationId: "my-inbox-notification-id", createdAt: "2021-10-06T01:45:56.558Z", triggeredAt: "2021-10-06T01:50:56.558Z", }, }; ``` If you want to easily identify this event in your code then you can use the type guard [`isTextMentionNotificationEvent`](/docs/api-reference/liveblocks-node#isTextMentionNotificationEvent). ##### Custom notification An event is triggered 30 minutes after the user has been notified of a custom event and has not seen the notification. All custom notification `kinds` are prefixed with `$` and are manually by you on the server. Learn more about [triggering custom notifications](/docs/api-reference/rest-api-endpoints#post-inbox-notifications-trigger). ```ts // Schema type CustomNotificationEvent = { type: "notification"; data: { channel: "email"; kind: "$yourKind"; // Can be any string starting with "$" as defined by the user projectId: string; roomId: string | null; userId: string; inboxNotificationId: string; // Date representing the time when the webhook event was created. createdAt: string; // Date representing the time when the notification itself was created. triggeredAt: string; }; }; // Example const customNotificationEvent = { type: "notification", data: { channel: "email", kind: "$fileUpload", projectId: "my-project-id", roomId: "my-room-id", userId: "my-user-id", inboxNotificationId: "my-inbox-notification-id", createdAt: "2021-10-06T01:45:56.558Z", triggeredAt: "2021-10-06T01:50:56.558Z", }, }; ``` If you want to easily identify this event in your code then you can use the type guard [`isCustomNotificationEvent`](/docs/api-reference/liveblocks-node#isCustomNotificationEvent). ## Use Cases With webhooks, you can subscribe to the events you are interested in, and be alerted of the change when it happens. Powerful ways to leverage webhooks with Liveblocks include: - Storage synchronization between room(s) and an internal database - Monitoring user activity in a room - Notifying the client if maximum concurrency has been reached Webhooks are an excellent way to reduce development time and the need for polling. By following the steps outlined in this guide, you’ll be able to configure, subscribe to, secure, and replay webhook events with Liveblocks. If you have any questions or need help using webhooks, please let us know [by email](mailto:support@liveblocks.io) or by joining our [Discord community](/discord)! We’re here to help! --- meta: title: "WebSocket infrastructure" parentTitle: "Platform" description: "Learn about the Liveblocks WebSocket infrastructure." --- ## WebSocket edge network The Liveblocks WebSocket edge network enables you to automatically create collaborative rooms in regions close to your users, reducing latency and improving end-user performance while enabling you to scale effortlessly.
Liveblocks WebSocket edge network
The Liveblocks WebSocket edge network primarily uses the [Cloudflare Workers](https://workers.cloudflare.com/) global network, spanning 300 cities and 100+ countries, enabling a ~50ms connection to 95% of the world’s internet-connected population. ## WebSocket connection engine The Liveblocks WebSocket connection engine ensures a reliable realtime experience for users. It handles offline, reconnections, and all sorts of edge-cases automatically so you don’t have to. See [API reference](/docs/api-reference/liveblocks-client#Room.getStatus) to learn more.
Liveblocks WebSocket connection engine
--- meta: title: "Plans" parentTitle: "Pricing" description: "Learn about the different plans available on Liveblocks." --- Liveblocks offers three plans: Free, Pro, and Enterprise. Each plan is designed to meet the needs of different types of developers, from personal projects to large enterprises. ## Free The Free plan is ideal for personal projects, prototyping, and testing. It includes access to Liveblocks’ collaboration infrastructure, ready-made features (Comments, Multiplayer, AI Agents, Notifications), and pre-built components, with up to monthly active rooms included. When you exceed your free limits, users won’t be able to join rooms. [Learn more about the Free plan](/docs/pricing/plans/free) ## Pro The Pro plan is designed for teams shipping a collaborative experiences in production with the ability to scale. It includes everything in Free, but enables you to remove the "Powered by Liveblocks" badge, and allows you to go beyond the included{/* prettier-ignore */} monthly active rooms via pay-as-you-go overage, and unlocks access to enterprise add-ons. [Learn more about the Pro plan](/docs/pricing/plans/pro) ### Add-ons | Add-on | Price | | ------------------------- | --------------------------------------------------------- | | Unlimited Version History | $100 per month Billed annually | | SAML SSO | $300 per month Billed annually | | SOC 2 report | $150 per month Billed annually | | HIPAA BAA | $350 per month Billed annually | ## Enterprise The Enterprise plan is tailored for organizations with advanced security, compliance, and scalability requirements. It includes everything in Pro, plus multi-region hosting, a management API, SCIM/directory sync, advanced permissions, custom usage allocations, and higher-touch support options such as SLAs and solution engineering. [Learn more about the Enterprise plan](/docs/pricing/plans/enterprise) ## General billing information ### Where do I understand my usage? You can monitor usage for your projects on the [usage page](/dashboard/usage) of your dashboard. ### What happens when I reach 100% usage? All plans receive notifications by email when they are approaching and exceed their [usage limits](/docs/platform/limits). Free plans will be paused when they exceed the included free tier usage. For Pro and Enterprise plans, when you reach 100% usage your projects are not automatically stopped. Rather, Liveblocks enables you to incur on-demand usage as your application grows. It’s important to be aware of [the usage page](/dashboard/usage) of your dashboard to see if you are approaching your limit. ## 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`](/docs/api-reference/liveblocks-react#LiveblocksProvider). ```tsx title="Set badge location" // "top-right", "bottom-right", "bottom-left", "top-left" ``` 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. Removing the "Powered by Liveblocks" badge on your projects requires a [paid plan](/pricing/). See the [pricing page](/pricing/) for more information. --- meta: title: "Liveblocks Enterprise plan" parentTitle: "Plans" description: "Learn about the Liveblocks Enterprise plan." --- The Enterprise plan is tailored for organizations with advanced security, compliance, and scalability requirements. It includes everything in Pro, plus multi-region hosting, a management API, SCIM/directory sync, advanced permissions, custom usage allocations, and higher-touch support options with SLAs and solution engineering. ## Security and compliance Built-in support for global security and data protection standards, including: - [Multi-factor authentication enforcement](/docs/platform/account-management/mfa) - [SAML SSO](/docs/platform/account-management/saml) - [Directory sync (SCIM)](/docs/platform/account-management/directory-sync) - SOC 2 Report - HIPAA BAA - Custom security reviews - 99.99% uptime SLA ## Observability and reporting Gain actionable insights with enhanced observability & logging: - Extended event log retention - External observability integrations (soon) ## Administration and support The Enterprise plan allows for streamlined team collaboration and offers robust support with: - Role-Based Access Control (RBAC) - A dedicated solution engineer - SLAs, including response time --- meta: title: "Liveblocks Free plan" parentTitle: "Plans" description: "Learn about the Liveblocks Free plan." --- The Free plan is aimed at developers with personal projects, and small-scale applications. It offers a generous set of features for individual users on a per month basis. | | Free included usage | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | | Monthly active rooms | | | Monthly active users | | | Projects | | | Team members | | | Simultaneous connections per room | | | Simultaneous connections per project | | | Monthly anonymous connections Public key only recommended for prototyping. Set up authentication. | | | Realtime data storage 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. | | | Realtime data stored per room | | | File storage 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. | | | Max file upload size | | | Comments stored | | | Monthly collaboration notifications Notifications automatically triggered from collaborative features like comment thread updates and @ mentions. | | | Monthly custom notifications Non-collaborative notifications that can be triggered from your own code. | | | Webhook event frequency The frequency at which `yDocUpdated` (Yjs) and `storageUpdated` (Liveblocks Storage) events are sent to your webhook endpoint. Learn more | | | Version history APIs, hooks, and pre-built components to access the previous versions of the realtime data stored in rooms. | | | Event log retention | | ## Billing cycle Even though the Free plan has no cost, usage limits follow a monthly billing cycle that starts on the 1st of each month and ends on the last day of the month. If you exceed your usage limits, you’ll need to wait until the next calendar month for limits to reset. Alternatively, you can [upgrade to Pro](/dashboard/billing?plan=pro) to access pay-as-you-go overage and avoid waiting for the next billing cycle. --- meta: title: "Liveblocks Pro plan" parentTitle: "Plans" description: "Learn about the Liveblocks Pro plan." --- The Pro plan is designed for teams shipping collaborative experiences in production with the ability to scale. It includes everything in Free, but removes the Liveblocks branding badge, allows you to go beyond the included{/* prettier-ignore */} monthly active rooms via pay-as-you-go overage, and unlocks access to enterprise add-ons. | Item | Pro included usage | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Monthly active rooms | included
| | Monthly active users | | | Projects | | | Team members | included
| | Simultaneous connections per room | | | Simultaneous connections per project | | | Monthly anonymous connections Public key only recommended for prototyping. Set up authentication. | | | Realtime data storage 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. | included
| | Realtime data stored per room | | | File storage 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. | included
| | Max file upload size | | | Comments stored | | | Monthly collaboration notifications Notifications automatically triggered from collaborative features like comment thread updates and @ mentions. | | | Monthly custom notifications Non-collaborative notifications that can be triggered from your own code. | included
| | Webhook event frequency The frequency at which `yDocUpdated` (Yjs) and `storageUpdated` (Liveblocks Storage) events are sent to your webhook endpoint. Learn more | | | Version history APIs, hooks, and pre-built components to access the previous versions of the realtime data stored in rooms. | Unlimited with Version History Plus add‑on | | Event log retention | | ## Billing cycle The Pro plan follows a monthly billing cycle that starts on the 1st of each month and ends on the last day of the month. Included usage resets at the beginning of each billing cycle. Any usage beyond the included amounts is charged on a pay-as-you-go basis and appears on your monthly invoice. ## Add-ons | Add-on | Price | | ------------------------- | --------------------------------------------------------- | | Unlimited Version History | $100 per month Billed annually | | SAML SSO | $300 per month Billed annually | | SOC 2 report | $150 per month Billed annually | | HIPAA BAA | $350 per month Billed annually | --- meta: title: "AI Agents" parentTitle: "Ready-made features" description: "The fastest way to add AI collaborators to your product" --- AI Agents let you embed AI teammates directly into your app. Agents can respond to messages, handle @mentions, perform actions, and collaborate inside your product—just like human users. Use a simple built-in copilot powered by any LLM, or connect your own agent framework such as n8n or LangChain. ## Key features ### Liveblocks AI Copilots Built-in agents you can configure directly in Liveblocks. - Works with popular LLMs (OpenAI, Anthropic, Gemini). - Add domain knowledge from text, PDFs, websites, or images (RAG). - Fully compatible with the `` React component. - Define behavior through simple prompts and settings. - No backend required—everything runs via the client SDK. ### Framework agents (private beta) Bring your own multi-agent logic or external workflows. - Connect n8n, LangChain, Crew AI, or your own agent system. - Receive room events (mentions, messages, actions) via webhooks. - Return actions that update UI, add comments, respond to users, or trigger workflows. - Ideal for multi-agent orchestration, automation, and complex pipelines. - Works with any server-side runtime or architecture. 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. --- meta: title: "Liveblocks AI Copilots" parentTitle: "AI Agents" description: "Easily integrate customizable AI copilots into your product" --- Embed customizable AI copilots into your product, enabling your users to collaborate with AI. Features include contextual chat, and AI toolbars, which can understand and modify your application state. ## Overview {/* TODO make more icons */} } /> } /> } /> } /> } /> } /> } /> } /> ## AI Copilots API Reference } /> } /> } /> } /> } /> ## Examples ## Try AI Copilots If you’d like to try AI Copilots, [book a demo with our team](/contact/sales), and we’ll reach out to you and enable it on your account. --- meta: title: "Copilots" parentTitle: "AI Copilots" description: "Customizable AI copilots" --- AI Copilots provides a default copilot for testing, but you should define a custom copilot in the [Liveblocks dashboard](/dashboard) for further development and production. This allows you to configure your AI model, system prompt, back-end knowledge, and more. ## Creating a copilot To create a copilot, open the [Liveblocks dashboard](/dashboard), select a project and click on the “AI Copilots” page. Click on the “Create copilot” button, and fill in the required fields.
Screenshot of the Liveblocks dashboard with the AI Copilots page and the Create copilot button
### Get your API key Copilots use API keys provided by your AI provider, which can be created and copied on their respective websites. Out of the box, we support [OpenAI](https://platform.openai.com/api-keys), [Anthropic](https://console.anthropic.com/settings/keys), and [Google](https://aistudio.google.com/app/api-keys), but, you can use other OpenAI-compatible APIs, such as [Groq](https://groq.com/api-keys) and [OpenRouter](https://openrouter.ai/settings/keys). ## Add the copilot to your application To use the new copilot in your application, copy your copilot ID from the dashboard.
Paste the copilot ID into [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) and [`useSendAiMessage`](/docs/api-reference/liveblocks-react#useSendAiMessage) to start using it. ```tsx import { AiChat } from "@liveblocks/react-ui"; function Chat() { return ( ); } ``` ```tsx import { useSendAiMessage } from "@liveblocks/react"; function SendMessage() { const sendAiMessage = useSendAiMessage("my-chat-id", { // +++ copilotId: "co_tUYtNctLAtUIAAIZBc1Zk", // +++ }); return ( ); } ``` ## Configuring your copilot Within the “General” tab you can configure a number of settings for your copilot. Settings slightly differ between providers. ### Name The name of your AI Copilot, which will be displayed in the dashboard. ```text title="Name" My AI Copilot ``` ### Provider This is the AI provider you want to use for your copilot. You can choose from [OpenAI](https://platform.openai.com/api-keys), [Anthropic](https://console.anthropic.com/settings/keys), and [Google](https://aistudio.google.com/app/api-keys), and other OpenAI-compatible APIs, such as [Groq](https://groq.com/api-keys). Using an OpenAI-compatible tool like OpenRouter allows you to [define fallback models](/docs/guides/how-to-use-fallback-ai-models-in-ai-copilots). ```text title="Provider" OpenAI ``` ### Model The AI model you want to use for your copilot, such as `GPT-4.1` or `Claude 4 Sonnet`. You can choose from the models supported by your provider. With third-party tools you can [define fallback models](/docs/guides/how-to-use-fallback-ai-models-in-ai-copilots). ```text title="Model" GPT-4.1 ``` ### Custom provider name If you’re using an OpenAI-compatible provider, you can define a custom provider name, which you may find on your provider’s website. If a custom provider name isn’t needed by your provider, you can use any string. ```text title="Custom provider name" custom-provider ``` ### Base URL If you’re using an OpenAI-compatible provider, you must define a base URL, which you can find on your provider’s website. ```text title="Base URL" https://openrouter.ai/api/v1/ ``` ### Reasoning effort, extended thinking, thinking budget Reasoning is supported by some models, allowing AI show its thought processes, and come up with more accurate answers. Some models allow you to select a reasoning effort, such as `low`, `medium`, or `high`, or a token budget for extended thinking. ```text title="Reasoning effort" low ``` ### Web search Toggle web searching, allowing your AI to query the internet for information. ```text title="Web search" on ``` ### Limit web search to specific domains When web search is enabled, limit the domains that the AI can search. Will search both `http://` and `https://` URLs. Subdomains are allowed. ```text title="Limit web search to specific domains" encyclopedia.com en.wikipedia.org ``` ### API key The API key for your provider. You can find keys on each provider’s website: [OpenAI](https://platform.openai.com), [Anthropic](https://console.anthropic.com), and [Google](https://aistudio.google.com). Also supports OpenAI-compatible APIs, such as [Groq](https://groq.com/) and [OpenRouter](https://openrouter.ai). All API keys are encrypted at rest, and never stored in plain text. ```text title="API key" sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ### System prompt The system prompt defines the behavior of the copilot, for example: ```text title="System prompt" You're a helpful assistant that can answer questions and help with tasks. ``` You can create more complex system prompts by using [Markdown formatting](https://www.markdownguide.org/basic-syntax/). ```text title="System prompt" # Role and objective You're a helpful assistant that can answer questions and help with tasks. ## Instructions - Always help the user as best you can. - Always use markdown formatting in your responses. - If you don't know the answer, tell the user to contact support. ## Examples User: How do I log in? Assistant: Navigate to the [login page](/login) and enter your email. User: What's the weather in Tokyo? Assistant: I don't know the answer to that question, please contact support. ``` ### When should AI use knowledge? This setting determines when AI will search back-end knowledge before responding. ```text title="When should AI use knowledge?" Whenever the user asks a question about code. ``` You can also write a more complex prompt, giving knowledge a name, which then allows you to refer to it in your system prompt. This can help reinforce AI behavior. ```text title="When should AI use knowledge?" Whenever the user asks a question about code. // +++ This is your **knowledge base**. // +++ ``` ```text title="System prompt" ## Instructions // +++ - When a user asks about React, searching your **knowledge base** is required. // +++ ... ``` ## Adding back-end knowledge Under the “Knowledge” tab you can add back-end knowledge to your copilot. Knowledge can be submitted as web pages or files, and your AI chat can query this knowledge when responding, allowing it to intelligently answer questions or perform tasks.
Screenshot of the Liveblocks dashboard, adding knowledge to a copilot
You can submit a whole website/sitemap for crawling, a single page, or PDF/image files. Crawled knowledge is not updated, and to update it, it must be resubmitted programmatically or via the dashboard. ## Configuring advanced settings Under the “Advanced” tab you can configure a number of advanced settings for your copilot. By default, Liveblocks does not pass default values to your provider, and leaves the options blank. | Setting | Description | Example value | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------- | | Max tokens The maximum number of tokens that the copilot can use per response. | Controls response length | `1024` | | Temperature Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. | Controls randomness in responses | `0.2` | | Top P Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both. | Alternative to temperature for controlling randomness | `0.9` | | Top K Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature. | Advanced sampling control | `200` | | Presence penalty Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model. | Controls repetition of existing content | `0.3` | | Frequency penalty Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model. | Controls repetition of words and phrases | `0.3` | | Stop sequences Sequences that will stop the generation of the text. If the model generates any of these sequences, it will stop generating further text. | Controls when text generation stops | `["\\n\\n", "Human:"]` | | Seed The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results. | Controls randomness for reproducible results | `42` | | Max retries Maximum number of retries. Set to 0 to disable retries. Default: 2. | Controls how many times to retry failed requests | `2` | ## Modifying copilots programmatically You aren’t limited to creating copilots from the dashboard—you can also create and modify copilots programmatically, allowing users or teams in your app to have their own individual copilots. ```ts import { Liveblocks } from "@liveblocks/node"; const liveblocks = new Liveblocks({ secret: "{{SECRET_KEY}}", }); // +++ const copilot = await liveblocks.createAiCopilot({ name: "My AI Assistant", systemPrompt: "You're a helpful assistant that can answer questions.", provider: "openai", providerModel: "gpt-4", providerApiKey: "sk-...", }); // +++ ``` This is made possible using the [Liveblocks Node.js client](/docs/api-reference/liveblocks-node#AI-Copilots) and [REST API](/docs/api-reference/rest-api-endpoints#AI), where a number of APIs are available for managing copilots. - [`getAiCopilots`](/docs/api-reference/liveblocks-node#get-ai-copilots) - [`createAiCopilot`](/docs/api-reference/liveblocks-node#create-ai-copilot) - [`getAiCopilot`](/docs/api-reference/liveblocks-node#get-ai-copilot) - [`updateAiCopilot`](/docs/api-reference/liveblocks-node#update-ai-copilot) - [`deleteAiCopilot`](/docs/api-reference/liveblocks-node#delete-ai-copilot) You can also manage each copilot’s knowledge sources. - [`createWebKnowledgeSource`](/docs/api-reference/liveblocks-node#create-web-knowledge-source) - [`createFileKnowledgeSource`](/docs/api-reference/liveblocks-node#create-file-knowledge-source) - [`getKnowledgeSources`](/docs/api-reference/liveblocks-node#get-knowledge-sources) - [`getKnowledgeSource`](/docs/api-reference/liveblocks-node#get-knowledge-source) - [`getFileKnowledgeSourceMarkdown`](/docs/api-reference/liveblocks-node#get-file-knowledge-source-markdown) - [`getWebKnowledgeSourceLinks`](/docs/api-reference/liveblocks-node#get-web-knowledge-source-links) - [`deleteWebKnowledgeSource`](/docs/api-reference/liveblocks-node#delete-web-knowledge-source) - [`deleteFileKnowledgeSource`](/docs/api-reference/liveblocks-node#delete-file-knowledge-source) --- meta: title: "Default components" parentTitle: "AI Copilots" description: "Ready-to-use customizable components" --- The default components included in AI Copilots are a great way to start building AI into your application. With these components you can render advanced AI chats, that understand your application state, modify it with actions, and render custom in-chat components. - Fully styled AI chat components, with an optional dark mode. - Pass in knowledge, add actions, and render custom components. - Customize through CSS variables and class names. - Localize and modify strings with overrides. ## AiChat The [`AiChat`][] component renders an AI chat, with a chat history and a composer for adding new messages.
AiChat
### Usage Get started by importing the component, and passing in a unique chat ID. ```tsx import { AiChat } from "@liveblocks/react-ui"; function Component() { return ; } ``` This will render an AI chat on the page, with a chat history, and a composer for adding new messages. Each chat is stored permanently, and can be accessed again later. {/* TODO list everything you can do with it */} ## AiTool When [registering a tool](/docs/ready-made-features/ai-copilots/tools#Registering-a-tool), the [`AiTool`][] component can be used to [show tool progress and results](/docs/ready-made-features/ai-copilots/tools) within AI chats. It shows the tool’s name, its stage (e.g. a spinner when executing, a checkmark when successful, etc.), and optionally an icon and custom content inside it. {/* TODO AiTool image */} ### Usage ```tsx import { defineAiTool } from "@liveblocks/client"; import { RegisterAiTool, AiTool, AiChat } from "@liveblocks/react-ui"; function App() { return ( <> { const { temperature, condition } = await __getWeather__( args.location ); return { data: { temperature, condition } }; }, render: ({ result }) => { return ( // +++ {result.data ? (
{result.data.temperature}°F - {result.data.condition}
) : null}
// +++ ); }, })} /> ); } ``` ## AiTool.Confirmation Inside of tools, the [`AiTool.Confirmation`][] component is a sub-component of [`AiTool`][] that renders confirm/deny buttons as part of a [human-in-the-loop tool action](/docs/ready-made-features/ai-copilots/tools#Default-confirmation-component). “Confirm” and Cancel” buttons are displayed to the user. {/* TODO AiTool.Confirmation image */} ### Usage ```tsx { return ( // +++ { await __deleteDocument__(documentId); return { data: { documentId }, description: "The user chose to delete the document", }; }} cancel={() => { return { data: { documentId }, description: "The user cancelled deleting the document", }; }} /> // +++ ); }, })} /> ``` ## Customization It’s possible to style and localize the default components: - Import dark mode styles. - Modify the style with CSS variables and class names. - Use overrides to change default text used in the components. Learn more under [styling and customization](/docs/ready-made-features/ai-copilots/styling-and-customization): [`aichat`]: /docs/api-reference/liveblocks-react-ui#AiChat [`aitool`]: /docs/api-reference/liveblocks-react-ui#AiTool [`aitool.confirmation`]: /docs/api-reference/liveblocks-react-ui#AiTool.Confirmation --- meta: title: "Features" parentTitle: "AI Copilots" description: "Learn about AI Copilots" --- Liveblocks AI Copilots provides customizable UI components that let your users interact with AI in a way that feels native to your product. Unlike basic chat widgets, Copilots are context-aware, collaborative, and capable of performing real tasks—such as editing content, navigating your app, or answering product-specific questions. They’re built for React developers, work with your chosen LLM, and integrate directly into your existing UI using flexible APIs and fully themeable components.
If you’d like to try AI Copilots, [book a demo with our team](/contact/sales), and we’ll reach out to you and enable it on your account. ## Features - **[Persistent chats](#persistent-chats)**: No database is required, each user’s chats are stored permanently. - **[Tools](#tools)**: Easily allow AI to interact with your application and modify its state. - **[Knowledge](#knowledge)**: Feed text and files into the AI so it understands the current context. - **[Ready-made UI](#ready-made-ui)**: Polished, easily customizable React components to add to your app. - **[Custom chat components](#custom-chat-components)**: Register React components that the AI can choose to render in-chat. - **[Text editor toolbar](#text-editor-toolbar)**: AI suggestions toolbar for your collaborative Tiptap text editor. - **[Manage AI copilots](#manage-ai-copilots)**: Configure AI providers and system prompts from our dashboard. - **[Supported AI providers](#supported-ai-providers)**: Anthropic, OpenAI, Google Gemini, and more. - **[Coming soon](#coming-soon)**: One-off prompts, attachments, MCP servers. ## Persistent chats [#persistent-chats] When building a chat interface, it’s important to save the chat history so that the user can continue the conversation from where they left off. All chats and messages are stored automatically by Liveblocks, and each user has [their own set of chats](/docs/ready-made-features/ai-copilots/hooks#List-a-user's-chats). No database is required, and messages are streamed into the chat in realtime using WebSockets.
An example showing a list of different chats, each with custom metadata, and a create new chat button.
Each page of your application can have multiple different chats, and it’s easy to switch between them, much like in ChatGPT. Each chat has a unique name, and can be given custom metadata, for example a custom title, description, tags, or anything you like. ### Automatic synchronization When the page refreshes, each user’s previous chats will load for them, and can be continued. If a user has your app open in multiple browser tabs, each tab will correctly display chats, and update in real-time. ## Tools [#tools] [Tools](/docs/ready-made-features/ai-copilots/tools) are a way to allow AI to make actions, modify your application state, interact with your front end, or render custom content with your own components. You can use them to extend the capabilities of AI Copilots beyond simple text-based interactions. For example, you may have tools that create new documents in your app, automatically fill in form data, invite members to a project, or anything else you like.
An example of a document with a chat app in the corner. The chat has run a tool, and has edited the document.
Tools are defined in your code, and are executed and/or rendered when the AI requests to use them. ### Tools work with our other products Through tools, you can integrate AI Copilots into our other products, such as [Comments](/docs/ready-made-features/comments), [Notifications](/docs/ready-made-features/notifications), and [Sync Datastore](/docs/platform/sync-datastore). For example, you can allow AI to add comments to your application, send notifications to other users, or add shapes to a collaborative drawing app. ## Knowledge [#knowledge] It’s simple to [add knowledge and context to your AI](/docs/ready-made-features/ai-copilots/knowledge), so that it understands the current document or page. You can submit webpages and files, such as documentation, that the copilot will deeply internalize, allowing it to reply intelligently. Additionally, certain models can also search the web.
An example of a document containing numbers. The AI chat in the corner has been asked about the numbers, and understands the context of the document.
## Ready-made UI [#ready-made-ui] Liveblocks AI Copilots includes a set of styled UI components that can be used to add an AI chat interface to your application. Messages are streamed in realtime.
A screenshot of the ready-made chat component, with messages and AI suggestion buttons.
We also provide [a number of hooks](/docs/ready-made-features/ai-copilots/hooks) that allow you to extend your chat, or create fully custom chat interfaces. ## Custom chat components [#custom-chat-components] Inside your chat, you can register custom React components that the AI can choose to render as a response. For example, if your app contains charts, AI can choose to render a custom chart component instead of a message. If you have multiple components, AI can choose which one to render.
An example of the AI rendering a custom chat component instead of a message. It's a graph component.
These components can be fully interactive, for example this chart component could have a button that lets you save it, and add it to a project. ## Text editor toolbar Using our [Text Editor integration for Tiptap](/docs/ready-made-features/text-editor/tiptap), we provide an AI toolbar that can be added to your collaborative text editor. This toolbar allows you to select text, and ask AI to make changes for you, for example fixing typos, and creating new paragraphs.
An example of the AI toolbar in a text editor, with an 'Ask Copilot' button.
Learn more about this React component under [`AiToolbar`](/docs/api-reference/liveblocks-react-tiptap#AiToolbar) ## Manage AI copilots [#manage-ai-copilots] The [Liveblocks dashboard](/dashboard) allows you to create, configure, and manage your AI copilots, each of which can be used in different parts of your application. You can select your [AI provider](#supported-ai-providers) (e.g. OpenAI, Anthropic), specify a system prompt, and pass in your secret key to get it working.
A screenshot of our dashboard, showing the settings for an AI copilot.
You can also fine-tune how each copilot interacts with users by adjusting each model’s settings, which are passed through directly to the AI provider. These can influence its creativity, consistency, and the safety of generated content. Each copilot can be configured independently, and tested live in the dashboard. ## Supported AI providers [#supported-ai-providers]
We support different AI providers
In our dashboard, you can create, configure, and manage copilots powered by different AI providers, each with different settings. The following providers are supported out-of-the-box: - Anthropic - OpenAI - Other OpenAI-compatible APIs - Google Gemini ### Reasoning models Reasoning models are supported by our built-in components, allowing models to show their thought processes. --- ## Coming soon [#coming-soon] ## One-off prompts [#one-off-prompts] One-off prompts are a way to add assorted AI features to your app, specifically features that don’t require a chat, such as an AI button on a page. [Tools](#tools) and [context](#knowledge) are supported, allowing AI to interact with your application, and understand its state. An example use case for a one-off prompt is a button that uses AI to fill in a form.
An example use for a one-off prompt, an AI button that fills in a table for you.
### Chat attachments Upload files into the chat, which AI can modify, or use for extra context. These files can be images, PDFs, text documents, or any file type your AI provider can understand. Files are automatically stored by Liveblocks.
A zoomed-in screenshot of a chat with an uploaded attachment, ready to send.
### MCP server integration MCP is a protocol for running AI agents on a server, allowing you to make various back ends calls to different services. In future, AI Copilots will support this. ### Additional providers We will be investigating additional providers, such as LangGraph, Crew AI, Bedrock, and Vertex AI. If there is a specific provider you’d like us to support, please [book a demo with our team](/contact/sales) and let us know more information in the text box. --- meta: title: "Hooks" parentTitle: "AI Copilots" description: "React hooks for building custom AI interfaces" --- The AI Copilots React hooks allow you to fetch, create, and modify AI chats, enabling you to build custom AI interfaces, even beyond our [default components](/docs/ready-made-features/ai-copilots/default-components). Chats are stored permanently and the infrastructure is handled for you. All hooks work optimistically, meaning they update immediately, before the server has synched. ## List a user’s chats Each [authenticated user](/docs/authentication) has their own private set of chats, and the [`useAiChats`](/docs/api-reference/liveblocks-react#useAiChats) hook fetches all AI chats created by the current user. It’s easy to [create a list of chats](/docs/api-reference/liveblocks-react#List-the-user's-chats-and-switch-between-them), with links or buttons that take you to each. ```tsx import { useAiChats } from "@liveblocks/react/suspense"; function ListChats() { // +++ const { chats } = useAiChats(); // +++ return ( ); } ``` Chats are [paginated](/docs/api-reference/liveblocks-react#useAiChats-pagination), returning 50 at a time, and you can [filter chats by metadata](/docs/api-reference/liveblocks-react#useAiChats-query). There are also ways to [handle errors](/docs/api-reference/liveblocks-react#useAiChats-error-handling). ## Create & delete chats The [`useCreateAiChat`](/docs/api-reference/liveblocks-react#useCreateAiChat) and [`useDeleteAiChat`](/docs/api-reference/liveblocks-react#useDeleteAiChat) hooks allow you to create and delete AI chats. When used in combination with the [`useAiChats`](/docs/api-reference/liveblocks-react#useAiChats) hook, you can add “New Chat” and “Delete Chat” buttons to your listing. ```tsx import { useAiChats, useCreateAiChat, useDeleteAiChat, } from "@liveblocks/react/suspense"; function ListChats() { const { chats } = useAiChats(); // +++ const createAiChat = useCreateAiChat(); const deleteAiChat = useDeleteAiChat(); // +++ return ( ); } ``` When an AI chat is created, a title is automatically generated from the first messages. You can optionally set this title, and add custom metadata to the chat. ```tsx createAiChat({ id: "my-ai-chat", // +++ title: "My AI Chat", metadata: { color: "red", tags: ["product", "engineering"], }, // +++ }); ``` {/* prettier-ignore */} {/* TODO add back later when we enable it ## List a chat’s messages The [`useAiChatMessages`](/docs/api-reference/liveblocks-react#useAiChatMessages) hook allows you to fetch all messages in a specific chat. Use this to display the main content of a chat. User and assistant messages can be rendered differently, and below we’re returning different UI for each. Other types of messages can be rendered separately too, for example tool calls, knowledge calls, custom components. ```tsx import { useAiChatMessages } from "@liveblocks/react/suspense"; function ChatMessages({ chatId }: { chatId: string }) { // +++ const { messages } = useAiChatMessages(chatId); // +++ return (
{messages.map((message) => { // +++ if (message.role === "user") { // +++ return ( // +++
{message.content.map((part) => (

👤 You: {part.text}

))}
// +++ ); } return ( // +++
{(message.contentSoFar ?? message.content).map((part) => (

🤖 Assistant: {part.text}

))}
// +++ ); })}
); } ``` Note that if you’re using our default [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) component, you don’t need to `useAiChatMessages`, as the messages are fetched for you. This hook is only required for building custom chat interfaces. \*/} ## Send messages to a chat The [`useSendAiMessage`](/docs/api-reference/liveblocks-react#useSendAiMessage) hook allows you to send messages directly to a chat, as if from a user. This works really well in combination with buttons in your UI, for example you may have a “Explain with AI” button. ```tsx import { useSendAiMessage } from "@liveblocks/react/suspense"; function ExplainWithAi() { // +++ const sendAiMessage = useSendAiMessage("my-chat-id", { copilotId: "co_h7GBa3...", }); // +++ return ( // +++ // +++ ); } ``` Additionally, when using [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) you can use the hook to display suggestion buttons in the [empty state](/docs/api-reference/liveblocks-react-ui#AiChat-placeholder). ```tsx import { useSendAiMessage } from "@liveblocks/react/suspense"; import { AiChat } from "@liveblocks/react-ui"; function Chat() { // +++ const sendAiMessage = useSendAiMessage("my-chat-id", { copilotId: "co_h7GBa3...", }); // +++ return (
How can I help you?
// +++ // +++ ), }} /> ); } ``` ## Get a chat’s information The [`useAiChat`](/docs/api-reference/liveblocks-react#useAiChat) hook allows you to fetch a [chat’s title](/docs/api-reference/liveblocks-react#Displaying-a-default-title) and [custom metadata](/docs/api-reference/liveblocks-react#Create-a-chat-with-a-custom-title-and-metadata). ```tsx import { useAiChat } from "@liveblocks/react/suspense"; function ChatTitle({ chatId }: { chatId: string }) { // +++ const { chat } = useAiChat(chatId); // +++ // +++ return

{chat.title || "Untitled chat"}

; // +++ } ``` ## Get a chat’s status The [`useAiChatStatus`](/docs/api-reference/liveblocks-react#useAiChatStatus) hook allows you to fetch the status of an AI chat, indicating whether it’s idle or currently generating content. This is helpful for displaying a loading state or freezing part of your app while content is generating. ```tsx import { useAiChatStatus } from "@liveblocks/react/suspense"; function ChatStatus({ chatId }: { chatId: string }) { // +++ const { status } = useAiChatStatus(chatId); // +++ // +++ return status === "idle" ?
🟢 Ready
:
🟡 Generating…
; // +++ } ``` ## Hook types [#hook-types] There are two different ways to use many Liveblocks hooks; with [React Suspense](https://react.dev/reference/react/Suspense), and without it. We recommend using the Suspense versions, as they often result in simpler code. ### Suspense hooks [#suspense-hooks] Using Suspense hooks means that any data retrieved, for example `chats` from `useAiChats`, will never be `undefined`, and your component will never see an error. ```tsx import { useAiChats } from "@liveblocks/react/suspense"; // Suspense: `chats` is always defined function MyAiChats() { const { chats } = useAiChats(); // [{ id: "th_sf8s6sh...", title: "...", ... }, ...] console.log(chats); } ``` To catch errors and display a loading screen, you can use [`ErrorBoundary`](https://www.npmjs.com/package/react-error-boundary) and [`ClientSideSuspense`](/docs/api-reference/liveblocks-react#suspense-avoid-ssr). ```tsx highlight="7-11" import { ClientSideSuspense } from "@liveblocks/react/suspense"; import { ErrorBoundary } from "react-error-boundary"; // Handle errors and loading state in the component above function Component() { return ( Error}> Loading...}> ); } ``` To use Suspense, make sure you’re exporting your hooks from `@liveblocks/react/suspense`. ```tsx // Suspense version of hooks import { useAiChats, useAiChatMessages } from "@liveblocks/react/suspense"; // ^^^^^^^^ ``` ### Regular hooks [#regular-hooks] The regular versions of Liveblocks hooks require you to check for `error` and `isLoading` properties. You can then handle these states in the same component. ```tsx import { useAiChats } from "@liveblocks/react"; // Handle errors and loading state in the same component function MyAiChats() { const { chats, error, isLoading } = useAiChats(); if (error) { return
Error
; } if (isLoading) { return
Loading...
; } // Non-Suspense: `chats` is only defined AFTER the `if` checks // [{ id: "th_sf8s6sh...", title: "...", ... }, ...] console.log(chats); } ``` To use the regular hooks, make sure you’re exporting from `@liveblocks/react`. ```tsx // Regular version of hooks import { useAiChats, useAiChatMessages } from "@liveblocks/react"; // ^^^^^^^^^^^^^^^^^ ``` --- meta: title: "Knowledge" parentTitle: "AI Copilots" description: "Add context and files to your AI copilots" --- Knowledge allows you to provide information to your AI so it can understand your application’s state, content, and domain-specific information, making responses more relevant and accurate. There are two different knowledge types—_front-end knowledge_, ideal for adding small pieces of contextual information, and _back-end knowledge_, designed for larger datasets such as entire websites and lengthy PDFs. Additionally, you can enable web search, allowing your AI to query the internet for information. ## Front-end knowledge Front-end knowledge is data that’s passed to your AI from the browser through [React](/docs/api-reference/liveblocks-react#RegisterAiKnowledge). This is most useful for passing contextual, user-specific, information, for example: - **User info**: The user’s local time, date, location, and language. - **Account info**: The user’s projects and which payment plan they’re on. - **Page info**: Which URL the user is viewing and the content of the current document. ### How it works [#front-end-knowledge-how-it-works] Front-end knowledge is string or JSON-serializable data that is passed to the AI with every message the user sends. The AI reads all front-end knowledge before responding. It’s easy to reach AI provider token limits with this approach, so limit your front-end knowledge to relatively small amounts of data, not multiple pages of text. Because front-end knowledge is simply passed along with every message, and is never specifically queried, no messages are displayed in the UI when it’s used. ### Adding front-end knowledge You can add front-end knowledge to your chat by using the [`RegisterAiKnowledge`](/docs/api-reference/liveblocks-react#RegisterAiKnowledge) component anywhere in your application. ```tsx import { RegisterAiKnowledge } from "@liveblocks/react"; import { AiChat } from "@liveblocks/react-ui"; function Chat() { return ( <> // +++ // +++ ); } ``` After adding this, the AI will understand that the user is on the Advanced Plan and be able to answer questions on it, for example: > User: _What's my current plan?_ > > Assistant: _You're on the Advanced Plan, it expires on January 1st, 2026._ ### Combine front-end knowledge with tools You can combine front-end knowledge with [tools](/docs/ready-made-features/ai-copilots/tools) to create an AI assistant that can take actions. For example, say you have a document on the current page. You can use knowledge to share the document content with the AI, then create a tool that allows AI to edit the document. ```tsx import { RegisterAiKnowledge } from "@liveblocks/react"; import { AiChat } from "@liveblocks/react-ui"; import { RegisterAiTool } from "@liveblocks/react"; import { defineAiTool } from "@liveblocks/client"; import { useState } from "react"; function Document() { // +++ const [document, setDocument] = useState("Hello world"); // +++ return ( <> { // +++ setDocument(args.text); // +++ return { data: {}, description: "Document updated" }; }, })} /> ); } ``` Learn about more ways to use it in our documentation under [`RegisterAiKnowledge`](/docs/api-reference/liveblocks-react#RegisterAiKnowledge). ## Back-end knowledge Back-end knowledge is data that’s passed to your AI from the server, through your [copilot](/docs/ready-made-features/ai-copilots/copilots#Adding-back-end-knowledge). You can submit PDF/image files, web pages, or entire websites which will be crawled and indexed. This is most useful for passing large amounts of information, for example: - **Knowledge bases**: Documentation, FAQs, and support tickets. - **Domain-specific data**: Detailed information that your AI must understand. - **Documents**: Submit PDF or image scans of reports, contracts, and invoices. ### How it works [#back-end-knowledge-how-it-works] Back-end knowledge uses Retrieval-Augmented Generation (RAG) search, and is triggered by a hidden tool call, meaning the AI will search through its back-end knowledge when it feels its relevant. You can define [when AI should use back-end knowledge](/docs/ready-made-features/ai-copilots/copilots#When-should-AI-use-knowledge) in your copilot settings, and AI will understand what should trigger a back-end knowledge query. The AI will rewrite your query, and may even run multiple queries in a row, each of which will be displayed in the [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) UI as a search. ### Adding back-end knowledge through the dashboard After [creating a copilot](/docs/ready-made-features/ai-copilots/copilots#Creating-a-copilot) in the [dashboard](/dashboard), you can navigate to the “Knowledge” tab to add back-end knowledge. Select which type of knowledge you’d like to submit, and enter your URLs or upload your files.
Screenshot of the Liveblocks dashboard, adding knowledge to a copilot
Make sure to use your copilot ID in [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) and [`useSendAiMessage`](/docs/api-reference/liveblocks-react#useSendAiMessage) to use the knowledge in your application. ### Adding back-end knowledge programmatically You aren’t limited to modifying knowledge through the dashboard—you can also add copilot knowledge programmatically, allowing users or teams in your app to have their own individual knowledge bases. ```ts import { Liveblocks } from "@liveblocks/node"; const liveblocks = new Liveblocks({ secret: "{{SECRET_KEY}}", }); // +++ const { id } = await liveblocks.createWebKnowledgeSource({ copilotId: "co_abc123...", url: "https://example.com", type: "crawl", }); // +++ ``` This is made possible using the [Liveblocks Node.js client](/docs/api-reference/liveblocks-node#AI-Copilots) and [REST API](/docs/api-reference/rest-api-endpoints#AI), where a number of APIs are available for managing copilots and knowledge sources. - [`createWebKnowledgeSource`](/docs/api-reference/liveblocks-node#create-web-knowledge-source) - [`createFileKnowledgeSource`](/docs/api-reference/liveblocks-node#create-file-knowledge-source) - [`getKnowledgeSources`](/docs/api-reference/liveblocks-node#get-knowledge-sources) - [`getKnowledgeSource`](/docs/api-reference/liveblocks-node#get-knowledge-source) - [`getFileKnowledgeSourceMarkdown`](/docs/api-reference/liveblocks-node#get-file-knowledge-source-markdown) - [`getWebKnowledgeSourceLinks`](/docs/api-reference/liveblocks-node#get-web-knowledge-source-links) - [`deleteWebKnowledgeSource`](/docs/api-reference/liveblocks-node#delete-web-knowledge-source) - [`deleteFileKnowledgeSource`](/docs/api-reference/liveblocks-node#delete-file-knowledge-source) ## Web search Web search is a feature that allows your AI to search the internet for information. It is disabled by default, and can be enabled by toggling the `webSearch` option in [your copilot settings](/docs/ready-made-features/ai-copilots/copilots#Web-search). ```text title="Web search" on ``` You can also [limit the domains that the AI can search](/docs/ready-made-features/ai-copilots/copilots#Limit-web-search-to-specific-domains) by setting an option on your copilot. ```text title="Limit web search to specific domains" encyclopedia.com en.wikipedia.org ``` It’s also possible to enable web search through the back-end with `allowedDomains`, for example using [`liveblocks.createAiCopilot`](/docs/api-reference/liveblocks-node#create-ai-copilot). ```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-...", providerOptions: { openai: { // Optional, restrict web search to specific domains // +++ webSearch: { allowedDomains: ["encyclopedia.com", "en.wikipedia.org"], }, // +++ }, }, }); ``` Not all models support web search, but most OpenAI and Anthropic models do. --- meta: title: "Styling and customization" parentTitle: "AI Copilots" description: "Customize the appearance and behavior of AI Copilots" --- Styling [default components](/docs/ready-made-features/ai-copilots/default-components) is enabled through a range of means, such as CSS variables, class names, and more. It’s also possible to use [overrides](#Overrides-and-localization) to modify any strings used in the default components, which is especially helpful for localization. ## Default components To add the default components’ theme, import the [default styles](/docs/api-reference/liveblocks-react-ui#Default-styles) CSS file. ```ts import "@liveblocks/react-ui/styles.css"; ``` You can also import one of two CSS files to enable [dark mode](/docs/api-reference/liveblocks-react-ui#Dark-mode), depending on how you’d like to enable it. ```ts // Dark mode using the system theme with `prefers-color-scheme` import "@liveblocks/react-ui/styles/dark/media-query.css"; ``` ```ts // Dark mode using `className="dark"`, `data-theme="dark"`, or `data-dark="true"` import "@liveblocks/react-ui/styles/dark/attributes.css"; ``` ### CSS variables A number of [CSS variables](/docs/api-reference/liveblocks-react-ui#CSS-variables) can be used to customize colors, spacing, and more. This is our recommended path for styling the default components, as you can quickly and easily modify all components with just a few variables. ```css /* Styles all default components */ .lb-root { --lb-accent: purple; --lb-spacing: 1em; --lb-radius: 0; } ``` ### Class names Should you need deeper customization, [class names](/docs/api-reference/liveblocks-react-ui#Class-names) can be styled, some of which provide contextual data attributes. ```css .lb-ai-chat { /* Customize AI chat */ } .lb-ai-composer { /* Customize message composer */ } ``` ### Overrides and localization It’s possible to [override strings](/docs/api-reference/liveblocks-react-ui#Overrides) used in the default components, which has a couple of different uses, the first being localization. In this example, we’re globally setting the AI composer’s placeholder from “Ask anything…” to “Posez une question” for French users. ```tsx import { LiveblocksUiConfig } from "@liveblocks/react-ui"; export function App() { return ( {/* ... */} ); } ``` You can also override strings on a component basis, for example if you’d like to change the “Send” button tooltip to “Post message” in the chat’s message composer. ```tsx import { Composer } from "@liveblocks/react-ui"; function Component() { return ( ); } ``` {/* prettier-ignore */} {/* TODO add back later when we enable it ## Custom interfaces You can bypass the default components and build completely custom UI for your AI copilots, for example using [`useAiChatMessages`](/docs/api-reference/liveblocks-react#useAiChatMessages) and [`useSendAiMessage`](/docs/api-reference/liveblocks-react#useSendAiMessage) to build your own chat history and composer. ```tsx import { useAiChatMessages, useSendAiMessage, } from "@liveblocks/react/suspense"; function ChatMessages({ chatId }: { chatId: string }) { // +++ const { messages } = useAiChatMessages(chatId); const sendAiMessage = useSendAiMessage(chatId, { copilotId: "co_a7Gd3x..." }); // +++ return (
// +++ {messages.map((message) => { // +++ if (message.role === "user") { return (
{message.content.map((part) => (

👤 You: {part.text}

))}
); } return (
{(message.contentSoFar ?? message.content).map((part) => (

🤖 Assistant: {part.text}

))}
); })}
{ e.preventDefault(); // +++ sendAiMessage({ text: e.target.message.value }); // +++ e.target.reset(); }} >
); } ``` Learn more about what you can build under [hooks](/docs/ready-made-features/ai-copilots/hooks). \*/} --- meta: title: "Tools" parentTitle: "AI Copilots" description: "Allow AI to interact with your application" --- Tools allow AI to make actions, modify your application state, interact with your front-end, and render custom components within your AI chat. Use tools to extend the capabilities of AI Copilots beyond simple text, allowing autonomous and human-in-the-loop interactions. ## Tool use cases Tools can be used to create various different interactions inside of your AI chat, such as: - **Actions**: Autonomously perform actions like editing documents, redirecting users, sending emails. - **Custom components**: Render custom React components like forms, graphs, videos, callouts. - **Query actions**: AI can query your app, search documents, find pages, check invoices. - **Human-in-the-loop actions**: Show confirm/deny buttons before taking destructive actions. - **AI presence**: Tool results can be streamed in, allowing AI to show live updates in your app. ### How tools work You can define a list of tools in your application, and your AI can choose to use them whenever it decides they’re needed. Within each tool you can set certain parameters which AI will fill in for you. For example, a weather tool may have a `location` parameter, and AI may enter `"Paris"` as the value. Here’s an example of a tool call interaction: In your weather tool, `location` is defined as a `string` ```json { "location": { type: "string" } } ``` {" "} User asks about the weather in Paris ```js User: "What's the weather in Paris?" ``` AI calls the weather tool with `Paris` as the `location` ```js { "location": "Paris" } ``` {" "} You write code to fetch the weather for the `location` ```js execute: async ({ location }) => { // { "temperature": 20, "condition": "sunny" }; const weather = await __fetchWeather__(location); return { data: { weather }}; } ``` AI answers the user ```js AI: "It's sunny in Paris, with a temperature of 20°C." ``` When writing your [system prompt](/docs/ready-made-features/ai-copilots/copilots#System-prompt) you can suggest when certain tools should be used, helping AI respond as you like. This is just an example of a simple tool, but below we’ll detail how to create more complex tools that have confirm/deny dialogs, render custom components, query data, and more. ## Defining tools You can define a tool with [`defineAiTool`](/docs/api-reference/liveblocks-client#defineAiTool) and [`RegisterAiTool`](/docs/api-reference/liveblocks-react#RegisterAiTool). First, you first need to give your tool a unique name, and a description, which helps AI understand when to call it. You can place the component anywhere in your app. ```tsx import { RegisterAiTool } from "@liveblocks/react"; import { defineAiTool } from "@liveblocks/client"; import { AiChat } from "@liveblocks/react-ui"; function Chat() { return ( <> // +++ // +++ ); } ``` For AI to use your tools intelligently, parameters must be defined, which AI will fill in for you. Tools use [JSON schema](/docs/ready-made-features/ai-copilots/tools#Advanced-JSON-schema) to define these. For example, you can define `location` parameter as a `string`. ```tsx ``` To add functionality to your tool, a combination of `execute` and `render` functions are used. ```tsx { // ... }, render: ({ stage, partialArgs, args, result, respond }) => { // ... }, // +++ })} /> ``` In each of the following sections, different ways to implement `execute` and `render` are detailed. ## Actions If you’d like your AI to perform an action when the tool is called, you can use `execute` to define what should happen. The arguments passed to `execute` are the parameters defined in your tool, filled in by AI. After the tool has run, return any `data` you’d like to pass back to AI. ```tsx { const weather = await __fetchWeather__(location); return { data: { weather } }; }, // +++ })} /> ``` After running `execute`, AI will read the `data` object, and choose how to respond. Additionally, you can define a `description` to pass back to AI. This is a way to inform AI what has just taken place, so it can understand the context of the result, and what it should do next. This text will never be shown to the user. ```ts execute: async ({ location }) => { const weather = await __fetchWeather__(location); return { data: { weather }, // +++ description: "You've just fetched the weather, share the temperature in °C." // +++ }; }, ``` The [AI Calendar](/examples/ai-calendar/nextjs-ai-calendar) example contains an action that allows AI to create new calendar events. ### Display a loading message You can easily display a loading message while an action takes place using `render` and [`AiTool`](/docs/api-reference/liveblocks-react-ui#AiTool). You can also choose to display a message after the action has finished, as in the example below. ```tsx import { AiChat, AiTool } from "@liveblocks/react-ui"; import { RegisterAiTool } from "@liveblocks/react"; function Chat() { <> { const weather = await __fetchWeather__(location); return { data: { weather }, description: "You've just fetched the weather.", }; }, // +++ render: ({ stage }) => { // `execute` is still running if (stage !== "executed") { return ; } // `execute` has finished return ; }, // +++ })} /> ; } ``` [`AiTool`](/docs/api-reference/liveblocks-react-ui#AiTool) isn’t required here, as you can return any JSX, but it’s an easy way to match the styling of the default chat. Returning `null` will display nothing. ### Combine actions with front-end knowledge You can combine actions with [front-end knowledge](/docs/ready-made-features/ai-copilots/knowledge) to create an AI assistant that can take actions. For example, say you have a document on the current page. You can use knowledge to pass the document’s text to the AI, then create a tool that allows AI to edit the document. ```tsx import { RegisterAiKnowledge } from "@liveblocks/react"; import { AiChat } from "@liveblocks/react-ui"; import { RegisterAiTool } from "@liveblocks/react"; import { defineAiTool } from "@liveblocks/client"; import { useState } from "react"; function Document() { // +++ const [document, setDocument] = useState("Hello world"); // +++ return ( <> { // +++ setDocument(args.text); // +++ return { data: {}, description: "Document updated" }; }, render: ({ stage }) => { if (stage !== "executed") { return ; } return ; }, })} /> ); } ``` ## Custom components You can use tools to display custom components inside the chat with the `render` function. These don’t have to be simple components, but can be complex, like forms, graphs, videos, callouts. When displaying a simple component, include an `execute` function, even if it’s empty, otherwise the chat will assume it’s a [human-in-the-loop action](#Human-in-the-loop-actions). ```tsx {}, // +++ render: () => { return ; }, // +++ })} /> ``` You can go further than this, and allow AI to [create parameters](#Advanced-JSON-schema) which you can use in your custom component, for example `x` and `y` values on a graph. ```tsx {}, render: ({ args, stage }) => { if (stage !== "executed") { return
Loading...
; } // +++ return ; // +++ }, })} /> ``` AI will most likely write a response after using your tool, but you can prompt the AI to not respond by adding a `description` to `execute`. ```tsx { return { data: {}, // +++ description: "You’re displaying a graph. Do not respond further.", // +++ }; }, render: ({ args, stage }) => { if (stage !== "executed") { return
Loading...
; } // +++ return ; // +++ }, })} /> ``` The [AI Support Chat](/examples/ai-support/nextjs-ai-support) example uses custom components in its support ticket tool to display a contact form. ### Fetching data for custom components You can take custom components a step further by combining them with [actions](#Actions), and then showing the results inside the custom component. The `result` property contains the data returned from the action. ```tsx { // { "temperature": 20, "condition": "sunny" }; const weather = await __fetchWeather__(location); return { data: { weather } }; }, // +++ render: ({ stage, args, result }) => { if (stage !== "executed") { return
Fetching weather…
; } return ( ); }, // +++ })} /> ``` `args` contains the arguments passed to the tool from AI, and `result` contains the data returned from the tool. ## Query actions A helpful way to use tools is to allow AI to query data from your application, such as documents, pages, or other data sources. If your application already contains a search function, you can easily plug it into your tool to create a powerful AI assistant. For example, this tool can search through documents by title, folder, and category. ```tsx { const documents = await __queryDocuments__({ title, folder, category }); return { data: { documents }, description: documents.length > 0 ? `${documents.length} results` : "No results" }; }, // +++ render: ({ stage, args, result }) => { if (stage !== "executed") { return ; } return null; }, })} /> ``` Since the query is happening on the front-end, you don’t need to implement separate authentication for your AI tool—it can leverage the same APIs that your users are already authorized to access. The [AI Reports Dashboard](/examples/ai-dashboard-reports/nextjs-ai-dashboard-reports) example has a query transactions tool allowing for complex searching. ## Human-in-the-loop actions Human-in-the-loop actions allow the user to confirm or deny an action before it’s executed. This is particularly useful when it comes to destructive and stateful actions, such as deleting a document, or sending an email. Confirmable actions like these will freeze the chat until the user responds, either by confirming or cancelling the action. User asks to delete a document ```js User: "Can you delete my-document.txt?" ``` AI calls the delete document tool, “Confirm” and “deny” buttons are displayed _The chat waits for the user to respond._ The user clicks “Confirm” _The document is deleted._ The chat unfreezes and is ready to continue ```js AI: "I've deleted it! How else can I help you?" ``` To create confirmable actions, you must skip using the `execute` function, and instead move your logic to `render`, as we detail below. This will always freeze the chat until the user responds. The [AI Reports Dashboard](/examples/ai-dashboard-reports/nextjs-ai-dashboard-reports) example demonstrates human-in-the-loop actions with its invite member tool. ### Default confirmation component The easiest way to create confirmable actions, is to return the ready-made [`AiTool.Confirmation`](/docs/api-reference/liveblocks-react-ui#AiTool.Confirmation) component in `render`. The `confirm` and `cancel` callbacks work very similarly to `execute`, and are triggered when the user clicks “Confirm” or “Cancel”. ```tsx { return ( // +++ { await __deleteDocument__(documentId); return { data: { documentId }, description: "The user chose to delete the document", }; }} cancel={() => { return { data: { documentId }, description: "The user cancelled deleting the document", }; }} /> // +++ ); }, })} /> ``` In the `cancel` callback it’s important to let the AI know that the user cancelled the action, otherwise it may assume the action failed and try to run it again. ```js description: "The user cancelled deleting the document", ``` ### Building a custom confirmation component By utilizing the `respond` argument in `render`, and the different stages of a tool’s lifecycle, you can build a fully custom confirmation component. These are the different stages: 1. `receiving` - Displayed when AI is streaming in the parameters. 2. `executing` - Displayed when the chat is frozen, and is waiting for a response. 3. `executed` - Displayed after a response has been recorded. Here’s how to leverage the different stages to create a custom “send email” tool—note how `respond` is used similarly to `execute`. ```tsx { // `emailAddress` param is still streaming in, wait // +++ if (stage === "receiving") { return
Loading...
; } // +++ // The tool is waiting for `respond` to be called // +++ if (stage === "executing") { return (
{ e.preventDefault(); const message = e.target.message.value; await __sendEmail__(args.emailAddress, message); // Similar to `execute`/`confirm`, let AI know it succeeded respond({ data: { emailAddress: args.emailAddress, message }, description: "You sent an email for the user", }); }} >