--- meta: title: "@liveblocks/chat-sdk-adapter" parentTitle: "API Reference" description: "API Reference for the @liveblocks/chat-sdk-adapter package" alwaysShowAllNavigationLevels: false --- `@liveblocks/chat-sdk-adapter` is a [Chat SDK](https://chat-sdk.dev) platform adapter backed by [Liveblocks Comments](/docs/products/comments). It maps Liveblocks rooms, threads, and comments to the Chat SDK's `Channel` / `Thread` / `Message` model, allowing you to build conversational bots that read and post in Liveblocks comment threads. ## Installation ```bash npm install @liveblocks/chat-sdk-adapter chat ``` ## Prerequisites Before using this adapter, ensure you have: 1. A [Liveblocks project](/docs/get-started) with rooms using [Comments](/docs/products/comments). 2. A **secret key** (`sk_...`) from the Liveblocks dashboard for REST API calls. 3. A **webhook secret** (`whsec_...`) from the dashboard to verify webhook payloads. 4. Webhooks configured to subscribe to `commentCreated`, `commentReactionAdded`, and `commentReactionRemoved` events. 5. A stable `botUserId` that matches how you identify users in your app. ## createLiveblocksAdapter [#createLiveblocksAdapter] Factory function that creates a new `LiveblocksAdapter` instance. ```ts import { createLiveblocksAdapter } from "@liveblocks/chat-sdk-adapter"; const adapter = createLiveblocksAdapter({ apiKey: "{{SECRET_KEY}}", webhookSecret: "whsec_...", botUserId: "my-bot-user", botUserName: "MyBot", }); ``` ### Configuration options [#configuration] Liveblocks secret key (`sk_...`) for REST API calls. Webhook secret (`whsec_...`) from the dashboard. User ID used when the bot creates, edits, or reacts to comments. Should match your app's user identifiers. Display name for the bot. Resolves user IDs to user info for mentions. Returns an array of user info in the same order as the input IDs, or `undefined` to skip resolution. Resolves group IDs to group info for mentions. Returns an array of group info in the same order as the input IDs, or `undefined` to skip resolution. Chat SDK-compatible logger instance. #### Resolving mentions [#resolving-mentions] When comments contain @mentions, the adapter needs to resolve user and group IDs to display names. Use `resolveUsers` and `resolveGroupsInfo` to provide this mapping: ```ts const adapter = createLiveblocksAdapter({ apiKey: "{{SECRET_KEY}}", webhookSecret: "whsec_...", botUserId: "my-bot-user", resolveUsers: async ({ userIds }) => { const users = await getUsersFromDatabase(userIds); return users.map((user) => ({ name: user.fullName, avatar: user.avatarUrl, })); }, resolveGroupsInfo: async ({ groupIds }) => { const groups = await getGroupsFromDatabase(groupIds); return groups.map((group) => ({ name: group.displayName })); }, }); ``` ### Webhook events [#webhook-events] The adapter processes incoming Liveblocks webhook requests via the Chat SDK's webhook handler. Supported events: - `commentCreated` — Triggers message processing in the Chat SDK - `commentReactionAdded` — Triggers reaction handlers - `commentReactionRemoved` — Triggers reaction handlers ```ts export async function POST(request: Request) { return bot.webhooks.liveblocks(request, { waitUntil: (p) => void p, }); } ``` The adapter automatically verifies webhook signatures using the `webhookSecret` provided during configuration. Invalid requests receive a `401` response. ### ID encoding [#id-encoding] The adapter uses a prefixed encoding scheme for thread and channel IDs: - **Thread ID**: `liveblocks:{roomId}:{threadId}` - **Channel ID**: `liveblocks:{roomId}` #### encodeThreadId [#encodeThreadId] Encodes a Liveblocks room ID and thread ID into a single thread ID string. ```ts adapter.encodeThreadId(data: { roomId: string; threadId: string }): string ``` ```ts const encoded = adapter.encodeThreadId({ roomId: "my-room", threadId: "th_abc123", }); // "liveblocks:my-room:th_abc123" ``` #### decodeThreadId [#decodeThreadId] Decodes an encoded thread ID string back into its room ID and thread ID components. Throws an `Error` if the format is invalid. ```ts adapter.decodeThreadId(threadId: string): { roomId: string; threadId: string } ``` ```ts const { roomId, threadId } = adapter.decodeThreadId( "liveblocks:my-room:th_abc123" ); // roomId: "my-room" // threadId: "th_abc123" ``` Room IDs may contain colons (`:`), which are preserved during encoding/decoding. However, Liveblocks thread IDs must not contain colons as the last colon is used as the delimiter when decoding. ### Liveblocks-specific behavior [#liveblocks-specific] #### Reactions [#reactions] Liveblocks Comments only supports Unicode emoji. Custom emoji identifiers that cannot be resolved to Unicode will fail validation. ```ts await adapter.addReaction(threadId, messageId, "👍"); // Works await adapter.addReaction(threadId, messageId, "thumbs_up"); // Converted to 👍 ``` #### Typing indicators [#typing-indicators] The `startTyping` method is a no-op as typing indicators are not supported by Liveblocks Comments. ## Message format limitations [#limitations] Liveblocks Comments has a simpler content model than full Markdown. Content from the Chat SDK is automatically converted, but some formatting is flattened. Liveblocks Comments supports: - Paragraphs with inline formatting (bold, italic, code, strikethrough) - Links - @mentions (users and groups) The following are **not supported** and will be flattened to plain text: - Headings — Converted to paragraphs - Bullet and numbered lists — Converted to paragraphs - Code blocks — Converted to paragraphs - Tables — Converted to ASCII representation in a paragraph - HTML — Rendered as plain text Card payloads from the Chat SDK are converted to markdown/plain text (or use `fallbackText` if provided), then converted to a comment body. Interactivity is not preserved. ## Example [#example] Here's a complete example integrating the adapter with the Chat SDK: ```ts import { Chat } from "chat"; import { createLiveblocksAdapter, LiveblocksAdapter, } from "@liveblocks/chat-sdk-adapter"; import { createMemoryState } from "@chat-adapter/state-memory"; const bot = new Chat<{ liveblocks: LiveblocksAdapter }>({ userName: "MyBot", adapters: { liveblocks: createLiveblocksAdapter({ apiKey: "{{SECRET_KEY}}", webhookSecret: "whsec_...", botUserId: "my-bot-user", botUserName: "MyBot", resolveUsers: async ({ userIds }) => { const users = await getUsersFromDatabase(userIds); return users.map((user) => ({ name: user.fullName })); }, }), }, state: createMemoryState(), }); bot.onNewMention(async (thread, message) => { await thread.adapter.addReaction(thread.id, message.id, "👀"); await thread.post(`Hello, ${message.author.userName}!`); }); bot.onReaction(async (event) => { if (!event.added) return; await event.adapter.postMessage( event.threadId, `${event.user.userName} reacted with "${event.emoji.name}"` ); }); ``` ### Webhook handler (Next.js) ```ts import { bot } from "./bot"; export async function POST(request: Request) { return bot.webhooks.liveblocks(request, { waitUntil: (p) => void p, }); } ``` The `waitUntil` option is recommended for serverless environments (e.g., Vercel) to allow background processing of messages after the response is sent. --- 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). 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). Deprecated. For new rooms, use [`engine: 2`](#Client.enterRoom) instead. Engine 2 rooms have native support for streaming. This flag will be removed in a future version, but will continue to work for existing engine 1 rooms for now. [Learn more](/docs/guides/the-new-storage-engine-and-its-benefits). ### 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. Preferred storage engine version to use when creating the room. Only takes effect if the room doesn’t exist yet. The v2 Storage engine supports larger documents, is more performant, has native streaming support, and will become the default in the future. [Learn more](/docs/guides/about-the-new-storage-engine). #### 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.toJSON(); room.history.undo(); // { name: "Fido", age: 5 } pet.toJSON(); ``` #### 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.history.disable [#Room.history.disable] Executes a callback with history tracking temporarily disabled. Any storage mutations made inside the callback will be applied normally but will not appear on the undo/redo stacks. This is useful for background writes that should not be undoable, such as writing back results from agent updates or reconciling state from an external source. If the callback throws, the undo/redo stacks are left unchanged (as if the callback never ran). ```ts room.history.disable(() => { root.set("generatedText", result); }); ``` #### Batching When combining with [`room.batch`](#Room.batch), always place the batch _inside_ the `disable` call. If `batch` wraps `disable`, the batched mutations will still end up on the undo stack. ```ts // ✅ Disabling undo must happen around a batch room.history.disable(() => { room.batch(() => { root.set("x", 1); root.set("y", 2); }); }); // ❌ Batch wraps disable, mutations will still end up in the undo stack room.batch(() => { room.history.disable(() => { root.set("x", 1); root.set("y", 2); }); }); ``` #### Async The history API is synchronous. For long-running async tasks, call `history.disable` at each synchronous step rather than wrapping the entire async function: ```ts async function generateSummary() { room.history.disable(() => root.set("status", "generating")); const summary = await fetchSummaryFromAgent(); room.history.disable(() => { room.batch(() => { root.set("summary", summary); root.set("status", "done"); }); }); } ``` The return value of the callback. The callback to execute while history is disabled. ### 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 initial comment, see [defining comment metadata](#defining-comment-metadata). 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 const metadata: Liveblocks["ThreadMetadata"] = { color: "blue", page: 3, pinned: true, }; const thread = await room.createThread({ body, metadata }); ``` ### Room.deleteThread Deletes a thread by its ID. ```ts await room.deleteThread("th_xxx"); ``` _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 content of the comment, see [creating comment content](#creating-comment-content). The IDs of the comment’s attachments. Custom metadata to be attached to the comment, see [defining comment metadata](#defining-comment-metadata). #### 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!" }, ], }, // +++ ], }; ``` #### Defining comment metadata [#defining-comment-metadata] Custom metadata can be attached to each comment. `string`, `number`, and `boolean` properties are allowed. ```ts const metadata: Liveblocks["CommentMetadata"] = { priority: 2, reviewed: true, }; const comment = await room.createComment({ threadId: "th_xxx", body, metadata, }); ``` ### Room.editComment Edits a comment, replacing its existing comment body and optionally updating its attachments and metadata. Learn more about [creating comment content](#creating-comment-content). ```ts const comment = await room.editComment({ threadId: "th_xxx", commentId: "cm_xxx", body: { version: 1, content: [{ type: "paragraph", children: [{ text: "Hello" }] }], }, }); ``` The comment that has been edited. The ID of the thread containing the comment. The ID of the comment that’s being edited. The content of the comment, see [creating comment content](#creating-comment-content). The IDs of the comment’s attachments. Custom metadata to be attached to the comment. ### Room.editCommentMetadata Edits a comment’s custom metadata. Metadata can be a `string`, `number`, or `boolean`. To delete an existing metadata property, set its value to `null`. ```ts await room.editCommentMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: { tag: "important", priority: 2, flagged: true, }, }); ``` The comment metadata. The ID of the thread containing the comment. The ID of the comment. 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.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. ## Feeds ### Room.fetchFeeds Fetches feeds in the current room. Returns a paginated list of feeds with an optional cursor for fetching more. ```ts const { feeds, nextCursor } = await room.fetchFeeds(); // [{ feedId: "feed-1", metadata: {...}, timestamp: 1234567890 }, ...] console.log(feeds); ``` A number of options are available for filtering and pagination. ```ts const { feeds, nextCursor } = await room.fetchFeeds({ // Optional, cursor for pagination. Use nextCursor from previous response cursor: "abc123", // Optional, only return feeds created or updated after this timestamp (ms) since: 1234567890000, // Optional, limit the number of feeds to return limit: 50, // Optional, filter feeds by metadata. Only feeds with matching metadata are returned metadata: { channel: true, name: "My Feed", }, }); ``` Feeds within the current room. Cursor for fetching the next page of feeds. Optional cursor for pagination. Optional timestamp filter (ms). Only messages whose `createdAt` is at or after this value are included. Optional limit for the number of feeds to return. Optional filter for feeds by metadata. Only feeds with matching metadata are returned. ### Room.fetchFeedMessages Fetches messages for a specific feed in the current room. Returns a paginated list of messages with an optional cursor for fetching more. ```ts const { messages, nextCursor } = await room.fetchFeedMessages("my-feed-id"); // [{ id: "msg-1", timestamp: 1234567890, data: {...} }, ...] console.log(messages); ``` ```ts const { messages, nextCursor } = await room.fetchFeedMessages("my-feed-id", { // Optional, cursor for pagination cursor: "abc123", // Optional, only return messages created after this timestamp (ms) since: 1234567890000, // Optional, limit the number of messages to return limit: 50, }); ``` Messages within the feed. Cursor for fetching the next page of messages. ### Room.addFeed Adds a new feed to the room. Changes are synchronized in real-time to all connected clients. ```ts room.addFeed("my-feed-id"); // With optional metadata and timestamp room.addFeed("my-feed-id", { metadata: { name: "My Feed", channel: true }, timestamp: Date.now(), }); ``` The ID of the feed to create. Optional custom metadata for the feed. Optional timestamp in milliseconds. Defaults to current time if not provided. ### Room.updateFeed Updates the metadata of an existing feed. Changes are synchronized in real-time to all connected clients. ```ts room.updateFeed("my-feed-id", { name: "Updated Feed Name", updated: new Date().toISOString(), }); ``` The ID of the feed to update. The new metadata for the feed. ### Room.deleteFeed Deletes a feed from the room. Changes are synchronized in real-time to all connected clients. ```ts room.deleteFeed("my-feed-id"); ``` The ID of the feed to delete. ### Room.addFeedMessage Adds a new message to a feed. Changes are synchronized in real-time to all connected clients. ```ts room.addFeedMessage("my-feed-id", { role: "user", content: "Hello, world!", }); // With optional id and timestamp room.addFeedMessage( "my-feed-id", { role: "user", content: "Hello!" }, { id: "my-message-id", timestamp: Date.now(), } ); ``` The ID of the feed to add the message to. The message data. Optional message ID. One will be generated if not provided. Optional timestamp in milliseconds. Defaults to current time if not provided. ### Room.updateFeedMessage Updates an existing feed message. Changes are synchronized in real-time to all connected clients. ```ts room.updateFeedMessage("my-feed-id", "my-message-id", { role: "user", content: "Updated content", }); ``` The ID of the feed containing the message. The ID of the message to update. The new message data. ### Room.deleteFeedMessage Deletes a feed message. Changes are synchronized in real-time to all connected clients. ```ts room.deleteFeedMessage("my-feed-id", "my-message-id"); ``` The ID of the feed containing the message. The ID of the message to delete. ## 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. ### LiveObject.from [#LiveObject.from] Creates a new `LiveObject` from a plain JSON object, recursively converting nested objects to `LiveObject` instances and arrays to `LiveList` instances. ```ts const liveObject = LiveObject.from({ name: "Grace", hobbies: ["reading", "piano"], address: { city: "New York", zip: "10001" }, }); ``` This is equivalent to writing: ```ts const liveObject = new LiveObject({ name: "Grace", hobbies: new LiveList(["reading", "piano"]), address: new LiveObject({ city: "New York", zip: "10001" }), }); ``` A new `LiveObject` with deeply converted children. A plain JSON object to convert into a `LiveObject`. ### 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.toJSON(); ``` _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.toJSON(); ``` _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.toJSON(); ``` _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_ ### toJSON [#LiveObject.toJSON] Returns a JSON-compatible snapshot of this `LiveObject` and all its nested children. `LiveObject` values become plain objects, `LiveList` values become arrays, and `LiveMap` values also become plain objects (not `Map` instances). The result is cached and only recomputed when the contents change. ```ts const liveObject = new LiveObject({ firstName: "Grace", lastName: "Hopper", hobbies: new LiveList(["reading", "piano"]), }); // { firstName: "Grace", lastName: "Hopper", hobbies: ["reading", "piano"] } liveObject.toJSON(); ``` A plain JSON-compatible object. Always serializable — `JSON.stringify()` works out of the box. _None_ ### reconcile [#LiveObject.reconcile] Reconciles this `LiveObject` tree to match the given JSON object. Only mutates keys that actually changed. Keys present on this `LiveObject` but absent from the input will be deleted. Nested structures are recursively reconciled. ```ts const liveObject = new LiveObject({ name: "Grace", age: 85, nested: new LiveObject({ x: 1, y: 2 }), }); // Updates nested.y, deletes age (absent from input) liveObject.reconcile({ name: "Grace", nested: { x: 1, y: 99 } }); ``` _Nothing_ The target state to reconcile towards. Missing keys will be deleted. ### toImmutable [#LiveObject.toImmutable] This method has been replaced by [`.toJSON()`](#LiveObject.toJSON), which returns plain objects instead of `Map` instances for `LiveMap` values, making the result always valid JSON. Please migrate to `.toJSON()`. ### toObject [#LiveObject.toObject] Use `.toJSON()` instead. It’s faster, cached, and deeply converts all nested Live structures. ## 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.toJSON(); ``` 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.toJSON(); ``` _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_ ### toJSON [#LiveMap.toJSON] Returns a JSON-compatible snapshot of this `LiveMap` and all its nested children. `LiveObject` values become plain objects, `LiveList` values become arrays, and `LiveMap` values also become plain objects (not `Map` instances). The result is cached and only recomputed when the contents change. ```ts const map = new LiveMap([ ["florent", new LiveObject({ role: "engineer" })], ["marc", new LiveObject({ role: "designer" })], ]); // { florent: { role: "engineer" }, marc: { role: "designer" } } map.toJSON(); ``` A plain JSON-compatible object with string keys. _None_ ### toImmutable [#LiveMap.toImmutable] This method has been replaced by [`.toJSON()`](#LiveMap.toJSON), which returns plain objects instead of `Map` instances, making the result always valid JSON. Please migrate to `.toJSON()`. ## 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.toJSON(); ``` _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.toJSON(); ``` 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.toJSON(); ``` _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.toJSON(); ``` _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.toJSON(); ``` _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.toJSON(); ``` ### 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_ ### toJSON [#LiveList.toJSON] Returns a JSON-compatible snapshot of this `LiveList` and all its nested children. `LiveObject` values become plain objects, `LiveList` values become arrays, and `LiveMap` values also become plain objects (not `Map` instances). The result is cached and only recomputed when the contents change. ```ts const list = new LiveList([ new LiveObject({ name: "Olivier" }), new LiveObject({ name: "Vincent" }), ]); // [{ name: "Olivier" }, { name: "Vincent" }] list.toJSON(); ``` A plain JSON-compatible array. Always serializable — `JSON.stringify()` works out of the box. _None_ ### toImmutable [#LiveList.toImmutable] This method has been replaced by [`.toJSON()`](#LiveList.toJSON), which returns plain objects instead of `Map` instances for `LiveMap` values, making the result always valid JSON. Please migrate to `.toJSON()`. ### toArray [#LiveList.toArray] Use [`.toJSON()`](#LiveList.toJSON) instead. It’s faster, cached, and deeply converts all nested Live structures. ## 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 metadata set on comments CommentMetadata: {}; // Custom room info set with resolveRoomsInfo RoomInfo: {}; // Custom group info set with resolveGroupsInfo GroupInfo: {}; // Custom activities data for custom notification kinds ActivitiesData: {}; } } // Necessary if you have no imports/exports export {}; ``` Here are some example values that might be used. ```ts file="liveblocks.config.ts" import { LiveList } from "@liveblocks/client"; declare global { interface Liveblocks { // Each user’s Presence Presence: { // Example, real-time cursor coordinates cursor: { x: number; y: number }; }; // The Storage tree for the room Storage: { // Example, a conflict-free list animals: LiveList; }; UserMeta: { id: string; // Custom user info set when authenticating with a secret key info: { // Example properties name: string; avatar: string; }; }; // Custom events // Example has two events, using a union RoomEvent: { type: "PLAY" } | { type: "REACTION"; emoji: "🔥" }; // Custom metadata set on threads ThreadMetadata: { // Example, attaching coordinates to a thread x: number; y: number; }; // Custom metadata set on comments CommentMetadata: { // Example, attaching a tag and a spam flag to a comment tag: string; spam: boolean; }; // Custom room info set with resolveRoomsInfo RoomInfo: { // Example, rooms with a title and url title: string; url: string; }; // Custom group info set with resolveGroupsInfo GroupInfo: { // Example, groups with a name and a badge name: string; badge: string; }; // Custom activities data for custom notification kinds ActivitiesData: { // Example, a custom $alert kind $alert: { title: string; message: string; }; }; } } // Necessary if you have no imports/exports export {}; ``` ### Typing with client.enter Before Liveblocks 2.0, it was recommended to type your data by passing `Presence`, `Storage`, `UserMeta`, and `RoomEvents` types to [`client.enterRoom`][]. This is no longer [the recommended method](#Typing-your-data) for setting up Liveblocks, but it can still be helpful, for example you can use `client.enter` multiple times to create different room types, each with their own correctly typed hooks. ```ts import { LiveList } from "@liveblocks/client"; // Each user’s Presence type Presence = { cursor: { x: number; y: number }; }; // The Storage tree for the room type Storage = { animals: LiveList; }; // 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 [`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 organization organizationId: "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", }, } ); ``` Never cache your access token authentication endpoint, as your client will not function correctly. The Liveblocks client will cache results for you, only making requests to the endpoint if necessary, such as when the token has expired. ##### 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 // +++ organizationId: "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 organization organizationId: "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. `organizationId` (optional) is the organization 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 organization organizationId: "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", }, } ); ``` Never cache your access token authentication endpoint, as your client will not function correctly. The Liveblocks client will cache results for you, only making requests to the endpoint if necessary, such as when the token has expired. ##### 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 [organizations](/docs/authentication/organizations) and [accesses](/docs/authentication/access-tokens/permissions). For example, this user can only see resources in the `acme-corp` organization, 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 organization // +++ organizationId: "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 organization organizationId: "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 `organizationId` (optional) is the organization 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 organization organizationId: "my-organization-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 organization organizationId: "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 organization organizationId: "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); }); ``` #### Liveblocks.setPresence [#post-rooms-roomId-presence] Sets ephemeral presence for a user in a room without requiring a WebSocket connection. The presence data automatically expires after the specified TTL (time-to-live). This is useful for scenarios like showing an AI agent’s presence in a room. The presence is broadcast to all connected users in the room. This is a wrapper around the [Set Presence API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-presence) and returns no response on success. ```ts await liveblocks.setPresence("my-room-id", { userId: "agent-123", data: { status: "active", cursor: { x: 100, y: 200 }, }, userInfo: { name: "AI Assistant", avatar: "https://example.com/avatar.png", }, ttl: 60, // optional, 2–3599 seconds }); ``` - **userId** (required): The ID of the user to set presence for. - **data** (required): Presence data as a JSON object. - **userInfo** (optional): Metadata about the user or agent - **ttl** (optional): Time-to-live in seconds (minimum 2, maximum 3599). Defaults to 60. After this duration, the presence expires automatically. ### 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", organizationId: "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 organization organizationId: "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", organizationId: "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", organizationId: "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", organizationId: "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", organizationId: "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. ### Attachments #### Liveblocks.getAttachment [#get-rooms-roomId-attachments-attachmentId] Returns an attachment's metadata and a presigned download URL. Throws an error if the room or attachment isn't found. ```ts const attachment = await liveblocks.getAttachment({ roomId: "my-room-id", attachmentId: "at_d75sF3...", }); // { type: "attachment", id: "at_d75sF3...", name: "document.pdf", ... } console.log(attachment); // The presigned URL to download the attachment console.log(attachment.url); ``` Returns an `AttachmentWithUrl` object with the following properties: The type of the object. The attachment ID (starts with "at_"). The name of the attachment file. The MIME type of the attachment. The size of the attachment in bytes. A presigned URL to download the attachment. The expiration time of the presigned URL. ### 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, // ... }, }, }); ``` You can also convert a Markdown string to a `CommentBody` with [`markdownToCommentBody`](#markdown-to-comment-body). This method has a number of options, allowing for custom metadata and a creation date for the comment. ```ts const thread = await liveblocks.createThread({ roomId: "my-room-id", data: { // Optional, custom metadata properties metadata: { color: "blue", page: 3, pinned: true, }, // Data for the first comment in the thread comment: { // The ID of the user that created the comment userId: "florent@example.com", // Optional, when the comment was created. createdAt: new Date(), // Optional, custom comment metadata metadata: { tag: "important", spam: false, }, // The comment's body, uses the `CommentBody` type body: { version: 1, content: [ /* The comment's body text goes here, see above */ ], }, }, }, }); // { type: "thread", id: "th_d75sF3...", ... } console.log(thread); ``` #### Liveblocks.getThread [#get-rooms-roomId-threads-threadId] Returns a thread. Throws an error if the room or thread isn’t found. This is a wrapper around the [Get Thread API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId) and returns the same response. ```ts const thread = await liveblocks.getThread({ roomId: "my-room-id", threadId: "th_d75sF3...", }); // { type: "thread", id: "th_d75sF3...", ... } console.log(thread); ``` #### Liveblocks.editThreadMetadata [#post-rooms-roomId-threads-threadId-metadata] Updates the metadata of a specific thread within a room. This method allows you to modify the metadata of a thread, including user information and the date of the last update. Throws an error if the room or thread isn’t found. This is a wrapper around the [Update Thread Metadata API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-metadata) and returns the updated metadata. ```ts const editedMetadata = await liveblocks.editThreadMetadata({ roomId: "my-room-id", threadId: "th_d75sF3...", data: { metadata: { color: "yellow", }, userId: "marc@example.com", updatedAt: new Date(), // Optional }, }); // { color: "yellow", page: 3, pinned: true } console.log(editedMetadata); ``` Metadata can be a `string`, `number`, or `boolean`. You can also use `null` to remove metadata from a thread. Here’s an example using every option. ```ts const editedMetadata = await liveblocks.editThreadMetadata({ roomId: "my-room-id", threadId: "th_d75sF3...", data: { // Custom metadata metadata: { // Metadata can be a string, number, or boolean title: "My thread title", page: 3, pinned: true, // Remove metadata with null color: null, }, // The ID of the user that updated the metadata userId: "marc@example.com", // Optional, the time the user updated the metadata updatedAt: new Date(), }, }); // { title: "My thread title", page: 3, pinned: true } console.log(editedMetadata); ``` #### Liveblocks.editCommentMetadata [#post-rooms-roomId-threads-threadId-comments-commentId-metadata] Updates the metadata of a specific comment within a thread. This method allows you to modify the metadata of a comment, including user information and the date of the last update. Throws an error if the room, thread, or comment isn’t found. This is a wrapper around the [Update Comment Metadata API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-metadata) and returns the updated metadata. ```ts const editedMetadata = await liveblocks.editCommentMetadata({ roomId: "my-room-id", threadId: "th_d75sF3...", commentId: "cm_agH76a...", data: { metadata: { spam: false, }, userId: "stacy@example.com", updatedAt: new Date(), // Optional }, }); // { spam: false } console.log(editedMetadata); ``` Metadata can be a `string`, `number`, or `boolean`. You can also use `null` to remove metadata from a comment. Here’s an example using every option. ```ts const editedMetadata = await liveblocks.editCommentMetadata({ roomId: "my-room-id", threadId: "th_d75sF3...", commentId: "cm_agH76a...", data: { // Custom metadata metadata: { // Metadata can be a string, number, or boolean tag: "important", priority: 2, spam: true, // Remove metadata with null assignedTo: null, }, // The ID of the user that updated the metadata userId: "stacy@example.com", // Optional, the time the user updated the metadata updatedAt: new Date(), }, }); // { tag: "important", priority: 2, flagged: true } console.log(editedMetadata); ``` #### Liveblocks.markThreadAsResolved [#post-rooms-roomId-threads-threadId-mark-as-resolved] Marks a thread as resolved, which means it sets the `resolved` property on the specified thread to `true`. Takes a `userId`, which is the ID of the user that resolved the thread. Throws an error if the room or thread isn’t found. This is a wrapper around the [Mark Thread As Resolved API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-resolved) and returns the same response. ```ts const thread = await liveblocks.markThreadAsResolved({ roomId: "my-room-id", threadId: "th_d75sF3...", data: { userId: "steven@example.com", }, }); // { type: "thread", id: "th_d75sF3...", ... } console.log(thread); ``` #### Liveblocks.markThreadAsUnresolved [#post-rooms-roomId-threads-threadId-mark-as-unresolved] Marks a thread as unresolved, which means it sets the `resolved` property on the specified thread to `false`. Takes a `userId`, which is the ID of the user that unresolved the thread. Throws an error if the room or thread isn’t found. This is a wrapper around the [Mark Thread As Unresolved API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-unresolved) and returns the same response. ```ts const thread = await liveblocks.markThreadAsUnresolved({ roomId: "my-room-id", threadId: "th_d75sF3...", data: { userId: "steven@example.com", }, }); // { type: "thread", id: "th_d75sF3...", ... } console.log(thread); ``` #### Liveblocks.deleteThread [#delete-rooms-roomId-threads-threadId] Deletes a thread. Throws an error if the room or thread isn’t found. This is a wrapper around the [Delete Thread API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-threads-threadId) and returns no response. ```ts await liveblocks.deleteThread({ roomId: "my-room-id", threadId: "th_d75sF3...", }); ``` #### Liveblocks.subscribeToThread [#post-rooms-roomId-threads-threadId-subscribe] Subscribes a user to a thread, meaning they will receive inbox notifications when new comments are posted. Throws an error if the room or thread isn’t found. This is a wrapper around the [Subscribe To Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-subscribe) and returns the same response. ```ts const subscription = await liveblocks.subscribeToThread({ roomId: "my-room-id", threadId: "th_d75sF3...", data: { userId: "steven@example.com", }, }); // { kind: "thread", subjectId: "th_d75sF3...", ... } console.log(subscription); ``` Subscribing will replace any existing subscription for the current thread [set at room-level](#post-rooms-roomId-users-userId-subscription-settings). This value can also be overridden by a room-level call that is run afterwards. ```ts const roomId = "my-room-id"; const userId = "steven@example.com"; // 1. Disables notifications for all threads await liveblocks.updateRoomSubscriptionSettings({ roomId, userId, data: { threads: "none", }, }); // 2. Enables notifications just for this thread, "th_d75sF3..." await liveblocks.subscribeToThread({ roomId, threadId: "th_d75sF3...", data: { userId }, }); // 3. Disables notifications for all threads, including "th_d75sF3..." await liveblocks.updateRoomSubscriptionSettings({ roomId, userId, data: { threads: "none", }, }); ``` #### Liveblocks.unsubscribeFromThread [#post-rooms-roomId-threads-threadId-unsubscribe] Unsubscribes a user from a thread, meaning they will no longer receive inbox notifications when new comments are posted. Throws an error if the room or thread isn’t found. This is a wrapper around the [Unsubscribe From Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-unsubscribe) and returns the same response. ```ts await liveblocks.unsubscribeFromThread({ roomId: "my-room-id", threadId: "th_d75sF3...", data: { userId: "steven@example.com", }, }); ``` Unsubscribing will replace any existing subscription for the current thread [set at room-level](#post-rooms-roomId-users-userId-subscription-settings). This value can also be overridden by a room-level call that is run afterwards. ```ts const roomId = "my-room-id"; const userId = "steven@example.com"; // 1. Enables notifications for all thread activity await liveblocks.updateRoomSubscriptionSettings({ roomId, userId, data: { threads: "all", }, }); // 2. Disables notifications just for this thread, "th_d75sF3..." await liveblocks.unsubscribeFromThread({ roomId, threadId: "th_d75sF3...", data: { userId }, }); // 3. Enables notifications for all thread activity, including "th_d75sF3..." await liveblocks.updateRoomSubscriptionSettings({ roomId, userId, data: { threads: "none", }, }); ``` #### Liveblocks.getThreadSubscriptions [#get-rooms-roomId-threads-threadId-subscriptions] Gets a thread’s subscriptions, returning a list of users that will receive notifications when new comments are posted. Throws an error if the room or thread isn’t found. This is a wrapper around the [Get Thread Subscriptions API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId-subscriptions) and returns the same response. ```ts const { data: subscriptions } = await liveblocks.getThreadSubscriptions({ roomId: "my-room-id", threadId: "th_d75sF3...", }); // [{ kind: "thread", subjectId: "th_d75sF3...", userId: "steven@example.com", ... }, ...] console.log(subscriptions); ``` #### Liveblocks.createComment [#post-rooms-roomId-threads-threadId-comments] Creates a new comment in a specific thread within a room. This method allows users to add comments to a conversation thread, specifying the user who made the comment and the content of the comment. This method is a wrapper around the [Create Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments) and returns the new comment. ```ts const comment = await liveblocks.createComment({ roomId: "my-room-id", threadId: "th_d75sF3...", data: { body: { version: 1, content: [ /* The comment's body text goes here, see below */ ], }, userId: "pierre@example.com", createdAt: new Date(), // Optional }, }); ``` A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `data.body`. ```tsx highlight="3-11,19" import { CommentBody } from "@liveblocks/node"; const body: CommentBody = { version: 1, content: [ { type: "paragraph", children: [{ text: "Hello " }, { text: "world", bold: true }], }, ], }; const comment = await liveblocks.createComment({ roomId: "my-room-id", threadId: "th_d75sF3...", data: { // The comment's body, uses the `CommentBody` type body, // ... }, }); ``` You can also convert a Markdown string to a `CommentBody` with [`markdownToCommentBody`](#markdown-to-comment-body). This method has a number of options, including the option to add a custom creation date and metadata to the comment. ```ts const comment = await liveblocks.createComment({ roomId: "my-room-id", threadId: "th_d75sF3...", data: { // The comment's body, uses the `CommentBody` type body: { version: 1, content: [ /* The comment's body text goes here, see above */ ], }, // The ID of the user that created the comment userId: "adrien@example.com", // Optional, the time the comment was created createdAt: new Date(), // Optional, custom comment metadata metadata: { tag: "important", reviewed: false, }, }, }); ``` #### Liveblocks.getComment [#get-rooms-roomId-threads-threadId-comments-commentId] Returns a comment. Throws an error if the room, thread, or comment isn’t found. This is a wrapper around the [Get Comment API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId-comments-commentId) and returns the same response. ```ts const comment = await liveblocks.getComment({ roomId: "my-room-id", threadId: "th_d75sF3...", commentId: "cm_agH76a...", }); // { type: "comment", threadId: "th_d75sF3...", ... } console.log(comment); ``` #### Liveblocks.editComment [#post-rooms-roomId-threads-threadId-comments-commentId] Edits an existing comment in a specific thread within a room. This method allows users to update the content of their previously posted comments, with the option to specify the time of the edit. Throws an error if the comment isn’t found. This is a wrapper around the [Edit Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId) and returns the updated comment. ```ts const editedComment = await liveblocks.editComment({ roomId: "my-room-id", threadId: "th_d75sF3...", commentId: "cm_agH76a...", data: { userId: "alicia@example.com", body: { version: 1, content: [ /* The comment's body text goes here, see below */ ], }, // Optional, the time the comment was edited editedAt: new Date(), // Optional, custom comment metadata metadata: { tag: "important", spam: false, }, }, }); // { type: "comment", threadId: "th_d75sF3...", ... } console.log(editedComment); ``` A comment’s body is an array of paragraphs, each containing child nodes. Here’s an example of how to construct a comment’s body, which can be submitted under `data.body`. ```tsx import { CommentBody } from "@liveblocks/node"; // +++ const body: CommentBody = { version: 1, content: [ { type: "paragraph", children: [{ text: "Hello " }, { text: "world", bold: true }], }, ], }; // +++ const editedComment = await liveblocks.editComment({ roomId: "my-room-id", threadId: "th_d75sF3...", commentId: "cm_agH76a...", data: { // The comment's body, uses the `CommentBody` type // +++ body, // +++ // ... }, }); ``` You can also convert a Markdown string to a `CommentBody` with [`markdownToCommentBody`](#markdown-to-comment-body). #### Liveblocks.deleteComment [#delete-rooms-roomId-threads-threadId-comments-commentId] Deletes a specific comment from a thread within a room. If there are no remaining comments in the thread, the thread is also deleted. This method throws an error if the comment isn’t found. This is a wrapper around the [Delete Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId) and returns no response. ```ts await liveblocks.deleteComment({ roomId: "my-room-id", threadId: "th_d75sF3...", commentId: "cm_agH76a...", }); ``` #### Liveblocks.addCommentReaction [#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction] Adds a reaction to a specific comment in a thread within a room. Throws an error if the comment isn’t found or if the user has already added the same reaction on the comment. This is a wrapper around the [Add Comment Reaction API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction) and returns the new reaction. ```ts const reaction = await liveblocks.addCommentReaction({ roomId: "my-room-id", threadId: "th_d75sF3...", commentId: "cm_agH76a...", data: { emoji: "👨‍👩‍👧", userId: "guillaume@example.com", createdAt: new Date(), // Optional, the time the reaction was added }, }); // { emoji: "👨‍👩‍👧", userId "guillaume@example.com", ... } console.log(reaction); ``` #### Liveblocks.removeCommentReaction [#post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction] Removes a reaction from a specific comment in a thread within a room. Throws an error if the comment reaction isn’t found. This is a wrapper around the [Remove Comment Reaction API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction) and returns no response. ```ts await liveblocks.removeCommentReaction({ roomId: "my-room-id", threadId: "th_d75sF3...", commentId: "cm_agH76a...", data: { emoji: "👨‍👩‍👧", userId: "steven@example.com", removedAt: new Date(), // Optional, the time the reaction is to be removed }, }); ``` #### Liveblocks.getRoomSubscriptionSettings [#get-rooms-roomId-users-userId-subscription-settings] Returns a user’s subscription settings for a specific room, specifying which `thread` and `textMention` inbox notifications they are set to receive. This is a wrapper around the [Get Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-users-userId-subscription-settings). ```ts const subscriptionSettings = await liveblocks.getRoomSubscriptionSettings({ roomId: "my-room-id", userId: "steven@example.com", }); // { threads: "all", textMentions: "mine" } console.log(subscriptionSettings); ``` For `"threads"`, these are the three possible values: - `"all"` Receive notifications for every activity in every thread. - `"replies_and_mentions"` Receive notifications for mentions and threads you’re participating in. - `"none"` No notifications are received. For `"textMentions"`, these are the two possible values: - `"mine"` Receive notifications for mentions of you. - `"none"` No notifications are received. #### Liveblocks.updateRoomSubscriptionSettings [#post-rooms-roomId-users-userId-subscription-settings] Updates a user’s subscription settings for a specific room, defining which `thread` and `textMention` inbox notifications they will receive. This is a wrapper around the [Update Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-users-userId-subscription-settings). ```ts const updatedSubscriptionSettings = await liveblocks.updateRoomSubscriptionSettings({ roomId: "my-room-id", userId: "steven@example.com", data: { threads: "replies_and_mentions", textMentions: "mine", }, }); // { threads: "replies_and_mentions", ... } console.log(updatedSubscriptionSettings); ``` For `"threads"`, these are the three possible values that can be set: - `"all"` Receive notifications for every activity in every thread. - `"replies_and_mentions"` Receive notifications for mentions and threads you’re participating in. - `"none"` No notifications are received. For `"textMentions"`, these are the two possible values that can be set: - `"mine"` Receive notifications for mentions of you. - `"none"` No notifications are received. ##### Replacing individual thread subscriptions Subscribing will replace any [existing thread subscriptions](#post-rooms-roomId-users-userId-subscription-settings) in the current room. This value can also be overridden by a room-level call that is run afterwards. ```ts const roomId = "my-room-id"; const userId = "steven@example.com"; // 1. Enables notifications just for this thread, "th_d75sF3..." await liveblocks.subscribeToThread({ roomId, threadId: "th_d75sF3...", data: { userId }, }); // 2. Disables notifications for all threads, including "th_d75sF3..." await liveblocks.updateRoomSubscriptionSettings({ roomId, userId, data: { threads: "none", }, }); ``` #### Liveblocks.deleteRoomSubscriptionSettings [#delete-rooms-roomId-users-userId-subscription-settings] Deletes a user’s subscription settings for a specific room. This is a wrapper around the [Delete Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-users-userId-subscription-settings). ```ts await liveblocks.deleteRoomSubscriptionSettings({ roomId: "my-room-id", userId: "steven@example.com", }); ``` #### Liveblocks.getUserRoomSubscriptionSettings [#get-users-userId-room-subscription-settings] Returns a list of a user’s subscription settings for all rooms. This is a wrapper around the [Get User Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#get-users-userId-room-subscription-settings). ```ts const { data: subscriptionSettings, nextCursor } = await liveblocks.getUserRoomSubscriptionSettings({ userId: "steven@example.com", // Optional, filter for a specific organization organizationId: "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, }); } ``` ### Feeds #### Liveblocks.getFeeds [#get-rooms-roomId-feeds] Returns a list of feeds in a room. This is a wrapper around the [Get Room Feeds API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-feeds) and returns the same response. ```ts const { data: feeds } = await liveblocks.getFeeds({ roomId: "my-room-id", }); // [{ feedId: "feed-1", metadata: {...}, timestamp: 1234567890 }, ...] console.log(feeds); ``` #### Liveblocks.createFeed [#post-rooms-roomId-feed] Creates a new feed in a room. This is a wrapper around the [Create Feed API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-feed) and returns the created feed. ```ts const feed = await liveblocks.createFeed({ roomId: "my-room-id", feedId: "my-feed-id", // Optional, custom metadata for the feed metadata: { name: "My Feed", channel: true, }, // Optional, timestamp in milliseconds. Defaults to current time if not provided timestamp: Date.now(), }); // { feedId: "my-feed-id", metadata: {...}, timestamp: 1234567890 } console.log(feed); ``` #### Liveblocks.getFeed [#get-rooms-roomId-feeds-feedId] Returns a feed by its ID. This is a wrapper around the [Get Feed API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-feeds-feedId) and returns the same response. ```ts const feed = await liveblocks.getFeed({ roomId: "my-room-id", feedId: "my-feed-id", }); // { feedId: "my-feed-id", metadata: {...}, timestamp: 1234567890 } console.log(feed); ``` #### Liveblocks.updateFeed [#patch-rooms-roomId-feeds-feedId] Updates the metadata of a feed. This is a wrapper around the [Update Feed API](/docs/api-reference/rest-api-endpoints#patch-rooms-roomId-feeds-feedId). ```ts await liveblocks.updateFeed({ roomId: "my-room-id", feedId: "my-feed-id", metadata: { name: "Updated Feed Name", updated: new Date().toISOString(), }, }); ``` #### Liveblocks.deleteFeed [#delete-rooms-roomId-feeds-feedId] Deletes a feed. This is a wrapper around the [Delete Feed API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-feeds-feedId). ```ts await liveblocks.deleteFeed({ roomId: "my-room-id", feedId: "my-feed-id", }); ``` #### Liveblocks.getFeedMessages [#get-rooms-roomId-feeds-feedId-messages] Returns a list of messages in a feed. This is a wrapper around the [Get Feed Messages API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-feeds-feedId-messages) and returns the same response. ```ts const { data: messages } = await liveblocks.getFeedMessages({ roomId: "my-room-id", feedId: "my-feed-id", }); // [{ id: "msg-1", timestamp: 1234567890, data: {...} }, ...] console.log(messages); ``` #### Liveblocks.createFeedMessage [#post-rooms-roomId-feeds-feedId-messages] Creates a new message in a feed. This is a wrapper around the [Create Feed Message API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-feeds-feedId-messages) and returns the created message. ```ts const message = await liveblocks.createFeedMessage({ roomId: "my-room-id", feedId: "my-feed-id", // The message data data: { role: "user", content: "Hello, world!", }, // Optional, custom message ID. One will be generated if not provided id: "my-message-id", // Optional, timestamp in milliseconds. Defaults to current time if not provided timestamp: Date.now(), }); // { id: "my-message-id", timestamp: 1234567890, data: {...} } console.log(message); ``` #### Liveblocks.updateFeedMessage [#patch-rooms-roomId-feeds-feedId-messages-messageId] Updates a feed message. This is a wrapper around the [Update Feed Message API](/docs/api-reference/rest-api-endpoints#patch-rooms-roomId-feeds-feedId-messages-messageId). ```ts await liveblocks.updateFeedMessage({ roomId: "my-room-id", feedId: "my-feed-id", messageId: "my-message-id", // The message data data: { role: "user", content: "Updated content", }, // Optional, timestamp in milliseconds. Helps prevents race conditions, // messages that arrive after a newer message are ignored updatedAt: Date.now(), }); ``` #### Liveblocks.deleteFeedMessage [#delete-rooms-roomId-feeds-feedId-messages-messageId] Deletes a feed message. This is a wrapper around the [Delete Feed Message API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-feeds-feedId-messages-messageId). ```ts await liveblocks.deleteFeedMessage({ roomId: "my-room-id", feedId: "my-feed-id", messageId: "my-message-id", }); ``` ### 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 organization organizationId: "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 organization organizationId: "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 organization organizationId: "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 organization organizationId: "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.", // Optional, always retrieve knowledge sources on each query alwaysUseKnowledge: true, // 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, always retrieve knowledge sources on each query alwaysUseKnowledge: true, // 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" }]; }, }); ``` ### markdownToCommentBody [#markdown-to-comment-body] Converts a Markdown string into a `CommentBody` object that can be used to write comments with [`createThread`](#post-rooms-roomId-threads), [`createComment`](#post-rooms-roomId-threads-threadId-comments), or [`editComment`](#post-rooms-roomId-threads-threadId-comments-commentId). ```ts import { markdownToCommentBody } from "@liveblocks/node"; const body = markdownToCommentBody(` @stacy Can you review this update? You can add **bold**, _italics_, ~~strikethrough~~, and \`inline code\`. Read more in [the docs](https://liveblocks.io/docs). `); ``` This is a lossy conversion because `CommentBody` only supports paragraphs, inline text formatting (bold, italic, strikethrough, code), links, and `@mentions`. Unsupported features like headings, lists, tables, or blockquotes are kept as plain text. ### 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 webhook 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 metadata of the comment 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`][]. You can also use [`Thread`][]'s [`components`](/docs/api-reference/liveblocks-react-ui#Thread-components) prop to customize individual comments, or build a fully custom `Thread` component using our [Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment). ```tsx import { Comment, Thread } from "@liveblocks/react-ui"; ( // +++ ( ), }} /> // +++ ), }} />; ``` #### 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`][]. You can also use [`Thread`][]'s [`components`](/docs/api-reference/liveblocks-react-ui#Thread-components) prop to customize individual comments, or build a fully custom `Thread` component using our [Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment). ```tsx import { Comment, Thread } from "@liveblocks/react-ui"; ( // +++ ( ), }} /> // +++ ), }} />; ``` ##### 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-flow" parentTitle: "API Reference" description: "API Reference for the @liveblocks/react-flow package" alwaysShowAllNavigationLevels: false --- `@liveblocks/react-flow` provides you with [React](https://react.dev/) hooks and components that add collaboration to any [React Flow](https://reactflow.dev/) diagram. It adds multiplayer data syncing, document persistence on the cloud, and realtime cursors. Read our [get started guide](/docs/get-started/nextjs-react-flow) to learn more. ## Setup If you’re not already using React Flow, follow their [guide](https://reactflow.dev/learn) to get started. Install it and include its base styles. ```bash npm install @xyflow/react ``` ```tsx import "@xyflow/react/dist/style.css"; ``` Install Liveblocks’ packages: ```bash npm install @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/react-flow ``` Import and use the [`useLiveblocksFlow`](#useLiveblocksFlow) hook to make React Flow collaborative: ```tsx "use client"; import { ReactFlow } from "@xyflow/react"; import { RoomProvider } from "@liveblocks/react"; // +++ import { useLiveblocksFlow } from "@liveblocks/react-flow"; // +++ import "@xyflow/react/dist/style.css"; function Flow() { // +++ const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onDelete, isLoading, } = useLiveblocksFlow(); // +++ if (isLoading) { return
Loading…
; } return ( // +++ // +++ ); } export function App() { return ( ); } ``` Then, import and add the [`Cursors`](#Cursors) component (alongside Liveblocks’ styles) to add realtime cursors inside React Flow’s canvas: ```tsx "use client"; import { ReactFlow } from "@xyflow/react"; import { RoomProvider } from "@liveblocks/react"; // +++ import { useLiveblocksFlow, Cursors } from "@liveblocks/react-flow"; // +++ import "@xyflow/react/dist/style.css"; // +++ import "@liveblocks/react-ui/styles.css"; import "@liveblocks/react-flow/styles.css"; // +++ function Flow() { const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onDelete, isLoading, } = useLiveblocksFlow(); if (isLoading) { return
Loading…
; } return ( // +++ // +++ ); } export function App() { return ( ); } ``` ## useLiveblocksFlow This hook returns a controlled [React Flow](https://reactflow.dev/) state made collaborative using Liveblocks Storage. You can pass initial nodes and edges to the hook which will be set when entering the room for the first time. ```tsx "use client"; import { ReactFlow } from "@xyflow/react"; import { RoomProvider } from "@liveblocks/react"; // +++ import { useLiveblocksFlow } from "@liveblocks/react-flow"; // +++ import "@xyflow/react/dist/style.css"; function Flow() { // +++ const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onDelete, isLoading, } = useLiveblocksFlow({ nodes: { initial: [ { id: "1", type: "input", data: { label: "Node 1" }, position: { x: 250, y: 25 }, }, { id: "2", data: { label: "Node 2" }, position: { x: 100, y: 125 }, }, ], // sync: { "*": { label: false } }, }, edges: { initial: [{ id: "e1-2", source: "1", target: "2" }], // sync: { "*": { ... } }, }, }); // +++ if (isLoading) { return
Loading…
; } return ( ); } export function App() { return ( ); } ``` Default nodes used when the room has no data yet. Per-type sync configuration for node `data` keys. See [Sync config](#sync-config). Default edges used when the room has no data yet. Per-type sync configuration for edge `data` keys. See [Sync config](#sync-config). The key used to store the flow in Liveblocks Storage. Defaults to `"flow"`. See [`storageKey`](#storageKey). When `true`, suspends until the diagram is ready. Learn more about this in the [Suspense](#suspense) section. The options passed to the hook (initial nodes, edges, storage key, Suspense, etc.) are read once when the hook mounts. Later changes to those options will not take effect. Current nodes, `null` while loading unless [using Suspense](#suspense), in which case it is always an array. Current edges, `null` while loading unless [using Suspense](#suspense), in which case it is always an array. Whether the diagram is still loading. When [using Suspense](#suspense), always `false` after the hook has resumed. Pass to React Flow’s `onNodesChange`. Pass to React Flow’s `onEdgesChange`. Pass to React Flow’s `onConnect`. Handles new edges. Pass to React Flow’s `onDelete`. Handles node and edge deletions atomically so that deleting a node and its related edges count as a single undoable action. ### Local state vs Storage Some React Flow fields are intentionally **not** written to Liveblocks Storage so each client keeps their own selection and interaction state: - **Nodes:** `selected`, `dragging`, `measured`, `resizing` - **Edges:** `selected` Everything else on nodes and edges (including `position`, `width` and `height`, `data`, handles, and edge endpoints) is synchronized through Storage. If you want specific keys inside `node.data` or `edge.data` to stay local-only too, use the [sync config](#sync-config). ### Undo / Redo [#undo-redo] Undo and redo are automatically enabled for the entire flow state. All synced changes to nodes and edges are recorded on the undo stack, including position changes, data updates, additions, and removals. A few things are handled automatically: - **Dragging** and **resizing** produce many live updates during a drag, but produce only a single action on the undo stack - **Deleting** nodes and edges in a single action will undo together - **Local-only** properties are not recorded on the undo stack To wire up undo/redo in your UI, just use Liveblocks’ normal [`useHistory`](/docs/api-reference/liveblocks-react#useHistory) hook: ```tsx import { useHistory } from "@liveblocks/react"; function Toolbar() { const history = useHistory(); return ( <> ); } ``` ### Custom nodes [#custom-nodes] [Custom nodes](https://reactflow.dev/learn/customization/custom-nodes) work like in any React Flow setup. ```tsx "use client"; import { useLiveblocksFlow } from "@liveblocks/react-flow"; import type { Node, NodeProps } from "@xyflow/react"; import { ReactFlow, useReactFlow } from "@xyflow/react"; import { memo, useCallback } from "react"; type TaskNode = Node<{ title: string }, "task">; const TaskNode = memo(({ id, data }: NodeProps) => { const { updateNode } = useReactFlow(); const rename = useCallback(() => { updateNode(id, (node) => ({ ...node, data: { ...node.data, title: "Updated" }, })); }, [id, updateNode]); return (
); }); function Flow() { const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onDelete } = useLiveblocksFlow({ suspense: true, nodes: { initial: [ { id: "1", type: "task", position: { x: 0, y: 0 }, data: { title: "Shared task" }, }, ], }, }); return ( ); } ``` ### Suspense [#suspense] By default, `useLiveblocksFlow` returns `isLoading: true`, `nodes: null`, and `edges: null` while loading. You can use the `suspense` option to suspend until the diagram is ready, when doing so, `nodes` and `edges` will always be arrays and `isLoading` will always be `false`. ```tsx "use client"; import { ReactFlow } from "@xyflow/react"; import { RoomProvider, ClientSideSuspense } from "@liveblocks/react"; import { useLiveblocksFlow } from "@liveblocks/react-flow"; import "@xyflow/react/dist/style.css"; function Flow() { const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onDelete, // +++ } = useLiveblocksFlow({ suspense: true }); // +++ return ( ); } export function App() { return ( // +++ Loading…
}> // +++ ); } ``` ### Storage key [#storageKey] By default, `useLiveblocksFlow` stores nodes and edges under key `"flow"` in Liveblocks Storage. Use the `storageKey` option to choose a different key or to support multiple diagrams in a single room. ```tsx "use client"; import { ReactFlow } from "@xyflow/react"; import { useLiveblocksFlow } from "@liveblocks/react-flow"; function FlowA() { // +++ const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onDelete } = useLiveblocksFlow({ suspense: true, storageKey: "flowA" }); // +++ return ( ); } function FlowB() { // +++ const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onDelete } = useLiveblocksFlow({ suspense: true, storageKey: "flowB" }); // +++ return ( ); } ``` ### Sync config for `node.data` [#sync-config] By default, every key inside a node or edge’s `data` object is getting deeply synced. Internally objects are stored as LiveObjects, arrays as LiveLists, etc, to enable fine-grained conflict-free merging automatically. If two users update different properties on the same node or edge, their changes will get merged without conflicts. For some data, this default behavior is not desirable. Each key in the config accepts a **sync mode**: | Mode | Behavior | | ---------- | ---------------------------------------------------------------------------------------------- | | `true` | Deeply sync and allow conflict-free merging (default). | | `false` | Keep value local-only. Not synced to other clients at all. Other clients will see `undefined`. | | `"atomic"` | Synced, but replaced as a whole (last-writer-wins). No automatic conflict resolution. | | `{ ... }` | Nested config. Applies recursively to sub-keys of the value. | Use `"*"` as a fallback for all node (or edge) types. ```tsx const { ... } = useLiveblocksFlow({ nodes: { sync: { // Applies to all node types "*": { label: false, // Don’t sync node.data.label color: "atomic", // Sync as a single value, replaced as-a-whole }, // Additional overrides for specific node types myCustomNode: { showPreview: false, // Don’t sync myCustomNode.data.showPreview }, }, }, edges: { sync: { "*": { hovered: false, // Don’t sync edge.data.hovered style: "atomic", // Sync as a single value, replaced as-a-whole }, }, }, }); ``` ## Cursors Add the `Cursors` component inside your `ReactFlow` component to add realtime cursors inside React Flow’s canvas. Also import Liveblocks’ styles when using it. ```tsx "use client"; import { RoomProvider } from "@liveblocks/react"; // +++ import { useLiveblocksFlow, Cursors } from "@liveblocks/react-flow"; // +++ import "@xyflow/react/dist/style.css"; // +++ import "@liveblocks/react-ui/styles.css"; import "@liveblocks/react-flow/styles.css"; // +++ function Flow() { const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onDelete, isLoading, } = useLiveblocksFlow(); if (isLoading) { return
Loading…
; } return ( // +++ // +++ ); } ``` It works similarly to `@liveblocks/react-ui`’s [`Cursors`](/docs/api-reference/liveblocks-react-ui#Cursors) component. By default, cursor coordinates are stored in Presence under `"cursor"`. Use `presenceKey` to support multiple diagrams in a single room. ### User information `Cursors` uses [`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers) to resolve each user’s information and then uses the `name` and `color` properties. ```tsx { // ["stacy@example.com", ...] console.log(userIds); // Get users from your back-end const users = await __fetchUsers__(userIds); // [{ name: "Stacy", color: "#22c55e"}, ...] console.log(users); // Return a list of users return users; }} // +++ > {/* ... */} ``` ### Customize cursors Pass a `Cursor` component through the `components` prop to control how each cursor is rendered. It receives `userId` and `connectionId` via its props. Its position and visibility are still handled by `Cursors`. ```tsx "use client"; import { Cursors } from "@liveblocks/react-flow"; import { Cursor, type CursorsCursorProps } from "@liveblocks/react-ui"; import { useUser } from "@liveblocks/react"; function MyCursor({ userId }: CursorsCursorProps) { const { user, isLoading } = useUser(userId); if (isLoading) { return null; } return ( {user.countryFlag} {user.name} ) : undefined } color={user?.color} /> ); } function Flow() { return ( // +++ // +++ ); } ``` ### Props [#Cursors-props] The key used to store cursor coordinates in users’ Presence. Override the component’s components. ## Updating your Flow from the server [#server-side] To update a React Flow from your Node.js back-end, use `mutateFlow`. ```tsx import { Liveblocks } from "@liveblocks/node"; // +++ import { mutateFlow } from "@liveblocks/react-flow/node"; // ~~~~ // +++ const client = new Liveblocks({ secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx", }); await mutateFlow( { client, roomId: "my-room-id" }, (flow) => { flow.addNode({ id: "1", position: { x: 0, y: 0 }, data: { label: "Hello" }, }); flow.addNode({ id: "2", position: { x: 100, y: 100 }, data: { label: "World" }, }); flow.addEdge({ id: "e1-2", source: "1", target: "2" }); } ); ``` ```bash npm install @liveblocks/node @liveblocks/react-flow ``` ### mutateFlow [#mutateFlow] `mutateFlow` opens a room’s flow for reading and mutating. A "flow" is a collection of nodes and edges stored in Liveblocks Storage. By default, your flow gets persisted in the Room’s Storage under the top-level key `"flow"`, but you can change this with the `storageKey` option. Even though internally `mutateFlow` uses Liveblocks Storage and its primitives like LiveObjects, LiveLists, and LiveMaps, you don’t need to work with those directly. You just work with React Flow nodes and edges as you normally would, and they get intelligently stored and synchronized via Live structures in the most efficient manner. A [Liveblocks Node client](/docs/api-reference/liveblocks-node#Liveblocks-client) instance. The ID of the room whose flow to mutate. The key used to store the flow in Liveblocks Storage. Defaults to `"flow"`. Must match the [`storageKey`](#storageKey) used on the client. Per-type sync configuration for node `data` keys. Must match the [sync config](#sync-config) used on the client. Per-type sync configuration for edge `data` keys. Must match the [sync config](#sync-config) used on the client. ### Custom node and edge types [#mutateFlow-custom-types] Pass your custom `Node` and `Edge` types as type parameters to get full type safety. ```ts import type { Node, Edge } from "@xyflow/react"; import { mutateFlow } from "@liveblocks/react-flow/node"; type TaskNode = Node<{ title: string; priority: number }, "task">; type CustomEdge = Edge<{ weight: number }, "weighted">; // +++ await mutateFlow( // +++ { client: liveblocks, roomId: "my-room-id" }, (flow) => { // flow.addNode, flow.updateNodeData, etc. are fully typed flow.addNode({ id: "t1", type: "task", position: { x: 0, y: 0 }, data: { title: "Ship it", priority: 1 }, }); } ); ``` ### MutableFlow API [#mutable-flow] The callback receives a `MutableFlow` instance. All reads reflect mutations made earlier in the same callback. #### flow.nodes Returns the current list of nodes as a readonly array. ```ts await mutateFlow({ client, roomId }, (flow) => { const allNodes = flow.nodes; // readonly Node[] }); ``` #### flow.edges Returns the current list of edges as a readonly array. ```ts await mutateFlow({ client, roomId }, (flow) => { const allEdges = flow.edges; // readonly Edge[] }); ``` #### flow.toJSON Returns a plain object with `nodes` and `edges` arrays. ```ts await mutateFlow({ client, roomId }, (flow) => { const snapshot = flow.toJSON(); // { nodes: [...], edges: [...] } }); ``` #### flow.getNode Returns a single node by ID, or `undefined` if not found. ```ts await mutateFlow({ client, roomId }, (flow) => { const node = flow.getNode("n1"); }); ``` #### flow.getEdge Returns a single edge by ID, or `undefined` if not found. ```ts await mutateFlow({ client, roomId }, (flow) => { const edge = flow.getEdge("e1"); }); ``` #### flow.addNode / flow.addNodes Adds one or more nodes. If a node with the same ID already exists, it is replaced. ```ts await mutateFlow({ client, roomId }, (flow) => { flow.addNode({ id: "1", position: { x: 0, y: 0 }, data: { label: "A" }, }); flow.addNodes([ { id: "2", position: { x: 100, y: 0 }, data: { label: "B" } }, { id: "3", position: { x: 200, y: 0 }, data: { label: "C" } }, ]); }); ``` #### flow.updateNode Updates a node by ID. Accepts a partial object to merge, or an updater function. No-op if the node does not exist. When using an updater function, always return a new object. Never mutate the provided value in-place. ```ts await mutateFlow({ client, roomId }, (flow) => { // With a partial object flow.updateNode("1", { position: { x: 100, y: 200 } }); // With an updater function flow.updateNode("1", (node) => ({ ...node, position: { x: node.position.x + 10, y: node.position.y }, })); }); ``` #### flow.updateNodeData Updates only a node’s `data` by ID. Accepts a partial object to merge, or an updater function. No-op if the node does not exist. When using an updater function, always return a new object. Never mutate the provided value in-place. ```ts await mutateFlow({ client, roomId }, (flow) => { // Merge partial data flow.updateNodeData("1", { label: "Updated" }); // With an updater function flow.updateNodeData("1", (data) => ({ ...data, color: "red", })); }); ``` #### flow.removeNode / flow.removeNodes Removes one or more nodes by ID. ```ts await mutateFlow({ client, roomId }, (flow) => { flow.removeNode("1"); flow.removeNodes(["2", "3"]); }); ``` #### flow.addEdge / flow.addEdges Adds one or more edges. If an edge with the same ID already exists, it is replaced. ```ts await mutateFlow({ client, roomId }, (flow) => { flow.addEdge({ id: "e1-2", source: "1", target: "2" }); flow.addEdges([ { id: "e2-3", source: "2", target: "3" }, { id: "e3-4", source: "3", target: "4" }, ]); }); ``` #### flow.updateEdge Updates an edge by ID. Accepts a partial object to merge, or an updater function. No-op if the edge does not exist. When using an updater function, always return a new object. Never mutate the provided value in-place. ```ts await mutateFlow({ client, roomId }, (flow) => { // With a partial object flow.updateEdge("e1-2", { label: "My edge" }); // With an updater function flow.updateEdge("e1-2", (edge) => ({ ...edge, label: "Updated", })); }); ``` #### flow.updateEdgeData Updates only an edge’s `data` by ID. Accepts a partial object to merge, or an updater function. No-op if the edge does not exist. When using an updater function, always return a new object. Never mutate the provided value in-place. ```ts await mutateFlow({ client, roomId }, (flow) => { // Merge partial data flow.updateEdgeData("e1-2", { weight: 5 }); // With an updater function flow.updateEdgeData("e1-2", (data) => ({ ...data, color: "red", })); }); ``` #### flow.removeEdge / flow.removeEdges Removes one or more edges by ID. ```ts await mutateFlow({ client, roomId }, (flow) => { flow.removeEdge("e1-2"); flow.removeEdges(["e2-3", "e3-4"]); }); ``` ### How mutations are flushed [#mutateFlow-flushing] The [`mutateFlow`][] function is built on top of [`Liveblocks.mutateStorage`][], but rather than giving you access to the Liveblocks Storage root directly, it gives you a convenient API tailored to React Flow—a `MutableFlow` instance you can use to read or mutate your nodes and edges directly. Under the hood, everything still gets stored and synced efficiently via Live structures (`LiveObject`, `LiveMap`, etc), but this is an implementation detail. Just like with [`Liveblocks.mutateStorage`][], your mutation callback can be long-running. During its execution, any mutations you make will periodically get flushed to the server in the background, and when your callback finishes, the final state will be flushed before the promise returned by `mutateFlow` resolves. [`mutateFlow`]: /docs/api-reference/liveblocks-react-flow#mutateFlow [`Liveblocks.mutateStorage`]: /docs/api-reference/liveblocks-node#mutate-storage --- 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 metadata of the comment 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`][]. You can also use [`Thread`][]'s [`components`](/docs/api-reference/liveblocks-react-ui#Thread-components) prop to customize individual comments, or build a fully custom `Thread` component using our [Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment). ```tsx import { Comment, Thread } from "@liveblocks/react-ui"; ( // +++ ( ), }} /> // +++ ), }} />; ``` #### 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`][]. You can also use [`Thread`][]'s [`components`](/docs/api-reference/liveblocks-react-ui#Thread-components) prop to customize individual comments, or build a fully custom `Thread` component using our [Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment). ```tsx import { Comment, Thread } from "@liveblocks/react-ui"; ( // +++ ( ), }} /> // +++ ), }} />; ``` ##### 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 metadata of the comment 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`][]. You can also use [`Thread`][]'s [`components`](/docs/api-reference/liveblocks-react-ui#Thread-components) prop to customize individual comments, or build a fully custom `Thread` component using our [Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment). ```tsx import { Comment, Thread } from "@liveblocks/react-ui"; ( // +++ ( ), }} /> // +++ ), }} />; ``` #### 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`][]. You can also use [`Thread`][]'s [`components`](/docs/api-reference/liveblocks-react-ui#Thread-components) prop to customize individual comments, or build a fully custom `Thread` component using our [Comment primitives](/docs/api-reference/liveblocks-react-ui#primitives-Comment). ```tsx import { Comment, Thread } from "@liveblocks/react-ui"; ( // +++ ( ), }} /> // +++ ), }} />; ``` ##### 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 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 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 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. ``` ##### Customize comments You can provide a custom `Comment` component via the `components` prop to fully customize how comments are rendered within a thread. This allows you to render a fully custom React component in the provided `Comment` slot, however it's often preferable to insert the default [`Comment`](/docs/api-reference/liveblocks-react-ui#Comment) component, and use its customization options instead. The `children` prop on `Comment` allows overriding or wrapping the comments’ content, while the `additionalContent` prop can be useful to render custom content integrated into the comments’ content, just below the comment body. The `body` prop is the same as the `children` prop but it only overrides the default rich-text comment body while still keeping attachments, reactions, and `additionalContent` as is. `Comment` also offers `avatar`, `author`, and `date` props to allow overriding or customizing the comment’s displayed avatar, author, and date respectively. `Comment.Avatar`, `Comment.Author`, and `Comment.Date` can be used to retain the default behavior and styles but with more control over them. ```tsx import { Comment, Thread } from "@liveblocks/react-ui"; (
} author={ Custom label } date={ {props.comment.editedAt && ( Edited )} } additionalContent={
Content below the comment's body (above reactions and attachments)
} {...props} > {({ children }) => (
{children}
Content below the comment's content (including reactions and attachments)
)}
), }} />; ``` ##### Customize 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. Whether to show the thread’s subscription status. 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. Override the component’s components. ###### components [#Thread-components] Override the component’s components, including providing a custom `Comment` component. The component used to display comments. {/* TODO: Document classes and data attributes */} #### FloatingThread Displays a floating thread attached to a trigger element. ```tsx ```
FloatingThread
`FloatingThread` can be combined with [`CommentPin`](#CommentPin) in canvas-like UIs. ```tsx ```
FloatingThread with CommentPin
##### Props [#FloatingThread-props] In addition to all [`Thread`](#Thread-props) props: The element which opens the floating thread. Whether the floating thread is initially open. Whether the floating thread is currently open. The event handler called when the open state changes. The preferred side of the trigger to render the floating thread on. The side offset in pixels from the trigger. How the floating thread is aligned against its trigger. The alignment offset in pixels. #### 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; }; CommentMetadata: { // Example types, e.g. tags, context, external IDs tag?: string; spam: boolean; slackMessageTs: string; }; // +++ // Other types // ... } } ``` ##### Replying to a thread If you provide a `threadId`, then submitting the composer will add a new reply to the thread. ```tsx import { Composer } from "@liveblocks/react-ui"; // Adds a new comment to a thread function Component({ threadId }) { return ; } ``` ##### Adding comment metadata If you’d like to attach custom metadata to a reply, you can add a `commentMetadata` prop. ```tsx import { Composer } from "@liveblocks/react-ui"; // Creates a new reply to an existing thread with custom metadata function Component({ threadId }) { return ( ); } ``` ##### 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 metadata of the comment to create or edit. 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. #### FloatingComposer Displays a floating composer attached to a trigger element. ```tsx ```
FloatingComposer
Use `metadata` to attach context to the thread when submitting (for example canvas coordinates or table cell IDs). `FloatingComposer` can be combined with [`CommentPin`](#CommentPin) in canvas-like UIs. ```tsx ```
FloatingComposer with CommentPin
##### Props [#FloatingComposer-props] In addition to all [`Composer`](#Composer-props) props (except `collapsed`, `onCollapsedChange`, and `defaultCollapsed`): The element which opens the floating composer. Whether the floating composer is initially open. Whether the floating composer is currently open. The event handler called when the open state changes. The preferred side of the trigger to render the floating composer on. The side offset in pixels from the trigger. How the floating composer is aligned against its trigger. The alignment offset in pixels. #### 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. The comment’s avatar. Can be combined with `Comment.Avatar` to easily follow default styles. The comment’s author. Can be combined with `Comment.Author` to easily follow default styles. The comment’s date. Can be combined with `Comment.Date` to easily follow default styles, or the `Timestamp` primitive for more control. 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. Additional content to display below the comment’s body. Override only the comment’s rich-text body. Receives the comment data and the default content as children. 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. Override the comment’s content. Receives the comment data and the default content as children. 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. ##### Comment.Avatar [#Comment.Avatar] Displays a comment’s avatar. Use this within the `avatar` prop to follow default styles while customizing the avatar. ```tsx
} /> ``` ###### Props [#Comment.Avatar-props] The user ID to display the avatar for. ##### Comment.Author [#Comment.Author] Displays a comment’s author. Use this within the `author` prop to follow default styles while customizing the author. ```tsx Custom label } /> ``` ###### Props [#Comment.Author-props] The user ID to display the author for. ##### Comment.DropdownItem [#Comment.DropdownItem] Displays a dropdown item in the comment’s dropdown menu. Use this within the `dropdownItems` prop to add custom actions. ```tsx console.log("Custom action")} icon={} > Custom action } /> ``` ###### Props [#Comment.DropdownItem-props] An optional icon displayed in this dropdown item. The event handler called when the dropdown item is selected. #### CommentPin Displays a comment pin that can be used as a trigger for `FloatingComposer` and `FloatingThread`, or anywhere else in your UI. ```tsx ```
CommentPin
Set the `userId` prop to display an avatar inside the pin, for example to represent the thread’s author. ```tsx ``` Use the `corner` prop to choose which corner the pin points to, it will move itself to always point to wherever it is positioned. ```tsx ``` You can either use the `size` prop or override `--lb-comment-pin-size` with CSS to change the pin’s size. ```tsx ``` Pass `children` to display custom content inside the pin. When children are provided, the `userId` prop is ignored. ```tsx ``` ##### Props [#CommentPin-props] The corner that points to the comment position. The user ID to optionally display an avatar for. Ignored if `children` is provided. The size of the pin. The padding within the pin. The content shown in the pin. If provided, the `userId` prop is ignored. ### 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) - [`useEditCommentMetadata`](/docs/api-reference/liveblocks-react#useEditCommentMetadata) - [`useDeleteComment`](/docs/api-reference/liveblocks-react#useDeleteComment) - [`useAddReaction`](/docs/api-reference/liveblocks-react#useAddReaction) - [`useRemoveReaction`](/docs/api-reference/liveblocks-react#useRemoveReaction) - [`useAttachmentUrl`](/docs/api-reference/liveblocks-react#useAttachmentUrl) ## Notifications ### Default components #### InboxNotification Displays a single inbox notification. ```tsx ```
InboxNotification
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). ## Presence ### Default components [#Presence-Components] #### Avatar Displays a user avatar from an image URL, or falls back to initials. ```tsx ``` You can also customize fallback content by passing children. This will take precedence over the default content. ```tsx {/* Custom icon */} ``` Use `outline` to add an outline around the avatar. ```tsx ``` Use `tooltip` to display additional content on hover. ```tsx stacy@example.com} /> ``` ##### Props [#Avatar-props] The URL of the avatar’s image. The name of the avatar. Whether and how the avatar should have an outline. The content to display in the avatar’s tooltip. Override the avatar’s content. #### AvatarStack Displays a stack of avatars for users currently present in the room. ```tsx ```
AvatarStack
You can include additional users (e.g. users who are invited to the room but not currently present) and control how many avatars are shown before grouping the rest as `+N`. ```tsx ``` User IDs (both from Presence and the `userIds` prop) are deduplicated so the same user present with multiple tabs for example will only be displayed once in the stack. You can either use the `size` prop or override `--lb-avatar-stack-size` with CSS to change the avatars’ size. ```tsx ``` You can use the `variant` prop to switch between default and outlined avatars. The outline color uses the resolved user’s `color` property by default. ```tsx ``` ##### User information `AvatarStack` uses [`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers) to resolve each user’s information and then uses the `name` and `avatar` properties. ```tsx { // ["stacy@example.com", ...] console.log(userIds); // Get users from your back-end const users = await __fetchUsers__(userIds); // [{ name: "Stacy", avatar: "https://example.com/stacy.png" }, ...] console.log(users); // Return a list of users return users; }} // +++ > {/* ... */} ``` ##### Props [#AvatarStack-props] Optional additional user IDs to include in the stack. The maximum number of items in the stack (at least 2). Set to `null` to show all avatars. The size of the avatars. The gap around the avatars. Whether the avatars should show an outline. Override the component’s strings. #### Cursors Displays multiplayer cursors for other users in the room while tracking your own pointer position. It works out-of-the-box with best practices built-in: - Cursors are positioned relative to the container’s size - Their coordinates are percentage-based instead of absolute pixels - Cursors are interpolated with springs to smooth their movement - Learn more about this in our article about [animating multiplayer cursors](/blog/how-to-animate-multiplayer-cursors) - Performance bottlenecks like re-renders or `position: absolute` layout thrashing are avoided ```tsx ```
Cursors
By default, cursor coordinates are stored in Presence under `"cursor"`. Use `presenceKey` to render multiple `Cursors` areas in a single room. If you need more control, you can use the single [`Cursor`](#Cursor) component and handle positioning manually with the Presence hooks in [`@liveblocks/react`](/docs/api-reference/liveblocks-react#Presence). ##### User information `Cursors` uses [`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers) to resolve each user’s information and then uses the `name` and `color` properties. ```tsx { // ["stacy@example.com", ...] console.log(userIds); // Get users from your back-end const users = await __fetchUsers__(userIds); // [{ name: "Stacy", color: "#22c55e" }, ...] console.log(users); // Return a list of users return users; }} // +++ > {/* ... */} ``` ##### Customize cursors Pass a `Cursor` component through the `components` prop to control how each cursor is rendered. It receives `userId` and `connectionId` via its props. Its position and visibility are still handled by `Cursors`. ```tsx import { Cursors, Cursor, type CursorsCursorProps } from "@liveblocks/react-ui"; import { useUser } from "@liveblocks/react"; function MyCursor({ userId }: CursorsCursorProps) { const { user, isLoading } = useUser(userId); if (isLoading) { return null; } return ( {user.countryFlag} {user.name} ) : undefined } color={user?.color} /> ); } ; ``` ##### Props [#Cursors-props] The key used to store cursor coordinates in users’ Presence. Override the component’s components. #### Cursor Displays a multiplayer cursor with a color and an optional label. ```tsx ```
Cursor
You can either use the `color` prop or override `--lb-cursor-color` with CSS to change the cursor’s color, by default it’s set to `--lb-accent`. The label’s text color is dynamically set to contrast with the cursor’s color automatically. ```tsx ``` It’s sized as 0×0px to make it easy to position it either via `transform` or `top`/`left`. ##### Props [#Cursor-props] The color of the cursor. A floating label to display next to the cursor. ### Hooks [#Presence-hooks] All hooks for Presence are in [`@liveblocks/react`](/docs/api-reference/liveblocks-react#Presence). ## 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 [#Version-History-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 ``` #### Override names Here's a list of all available override names, their descriptions, and default values. ##### Localization [#Overrides-Localization] Localization overrides can only be set on [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | -------- | -------------- | ------------- | | `locale` | Locale code | `"en"` | | `dir` | Text direction | `"ltr"` | ##### Global [#Overrides-Global] Global overrides can only be set on [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | --------------------------------- | ------------------------ | ---------------------------------- | | `USER_SELF` | Current user label | `"you"` | | `USER_UNKNOWN` | Unknown user label | `"Anonymous"` | | `LIST_REMAINING` | Remaining items | `"X more"` | | `LIST_REMAINING_USERS` | Remaining users | `"X others"` | | `LIST_REMAINING_COMMENTS` | Remaining comments | `"X more comments"` | | `EMOJI_PICKER_SEARCH_PLACEHOLDER` | Emoji search placeholder | `"Search…"` | | `EMOJI_PICKER_EMPTY` | Empty emoji results | `"No emoji found."` | | `EMOJI_PICKER_ERROR` | Emoji picker error | `"There was an error…"` | | `EMOJI_PICKER_CHANGE_SKIN_TONE` | Skin tone button label | `"Change skin tone"` | | `ATTACHMENT_TOO_LARGE` | File too large error | `"The file is larger than X"` | | `ATTACHMENT_ERROR` | Upload error | `"The file couldn't be uploaded."` | | `COPY_TO_CLIPBOARD` | Copy button label | `"Copy"` | ##### Composer [#Overrides-Composer] Composer overrides can be set on both [`Composer`](/docs/api-reference/liveblocks-react-ui#Composer) and [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | ---------------------------- | ----------------------- | -------------------------- | | `COMPOSER_INSERT_MENTION` | Mention button label | `"Mention someone"` | | `COMPOSER_INSERT_EMOJI` | Emoji button label | `"Add emoji"` | | `COMPOSER_ATTACH_FILES` | Attach button label | `"Attach files"` | | `COMPOSER_REMOVE_ATTACHMENT` | Remove attachment label | `"Remove attachment"` | | `COMPOSER_PLACEHOLDER` | Input placeholder | `"Write a comment…"` | | `COMPOSER_SEND` | Send button label | `"Send"` | | `COMPOSER_TOGGLE_MARK` | Format toggle labels | `"Bold"`, `"Italic"`, etc. | ##### Comment [#Overrides-Comment] Comment overrides can be set on both [`Comment`](/docs/api-reference/liveblocks-react-ui#Comment) and [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | ----------------------------------- | ----------------------- | ---------------------------------- | | `COMMENT_EDITED` | Edited indicator | `"(edited)"` | | `COMMENT_DELETED` | Deleted message | `"This comment has been deleted."` | | `COMMENT_MORE` | More actions label | `"More"` | | `COMMENT_EDIT` | Edit action label | `"Edit comment"` | | `COMMENT_EDIT_COMPOSER_PLACEHOLDER` | Edit placeholder | `"Edit comment…"` | | `COMMENT_EDIT_COMPOSER_CANCEL` | Cancel edit label | `"Cancel"` | | `COMMENT_EDIT_COMPOSER_SAVE` | Save edit label | `"Save"` | | `COMMENT_DELETE` | Delete action label | `"Delete comment"` | | `COMMENT_DELETE_ATTACHMENT` | Delete attachment label | `"Delete attachment"` | | `COMMENT_ADD_REACTION` | Add reaction label | `"Add reaction"` | | `COMMENT_REACTION_LIST` | Reaction list text | `"X reacted with Y"` | | `COMMENT_REACTION_DESCRIPTION` | Reaction description | `"X reactions, react with Y"` | ##### Thread [#Overrides-Thread] Thread overrides can be set on both [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) and [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | ---------------------------------- | ------------------------- | --------------------------- | | `THREAD_RESOLVE` | Resolve action label | `"Resolve thread"` | | `THREAD_UNRESOLVE` | Unresolve action label | `"Re-open thread"` | | `THREAD_SUBSCRIBE` | Subscribe action label | `"Subscribe to thread"` | | `THREAD_UNSUBSCRIBE` | Unsubscribe action label | `"Unsubscribe from thread"` | | `THREAD_NEW_INDICATOR` | New indicator label | `"New"` | | `THREAD_NEW_INDICATOR_DESCRIPTION` | New indicator description | `"New comments"` | | `THREAD_SHOW_MORE_COMMENTS` | Show more label | `"Show X more replies"` | | `THREAD_COMPOSER_PLACEHOLDER` | Reply placeholder | `"Reply to thread…"` | | `THREAD_COMPOSER_SEND` | Reply button label | `"Reply"` | ##### Inbox notification [#Overrides-Inbox-notification] Inbox notification overrides can be set on both [`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification) and [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | ----------------------------------------- | ------------------- | ------------------------ | | `INBOX_NOTIFICATION_MORE` | More actions label | `"More"` | | `INBOX_NOTIFICATION_MARK_AS_READ` | Mark read label | `"Mark as read"` | | `INBOX_NOTIFICATION_DELETE` | Delete action label | `"Delete notification"` | | `INBOX_NOTIFICATION_THREAD_COMMENTS_LIST` | Comments list text | `"X commented in Y"` | | `INBOX_NOTIFICATION_THREAD_MENTION` | Thread mention text | `"X mentioned you in Y"` | | `INBOX_NOTIFICATION_TEXT_MENTION` | Text mention text | `"X mentioned you in Y"` | ##### History version preview [#Overrides-History-version-preview] History version preview overrides can be set on both [`HistoryVersionSummary`](/docs/api-reference/liveblocks-react-ui#HistoryVersionSummary) and [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | -------------------------------------- | -------------------- | ----------------------- | | `HISTORY_VERSION_PREVIEW_AUTHORS_LIST` | Authors list text | `"Edits from X"` | | `HISTORY_VERSION_PREVIEW_RESTORE` | Restore button label | `"Restore"` | | `HISTORY_VERSION_PREVIEW_EMPTY` | Empty state text | `"No content."` | | `HISTORY_VERSION_PREVIEW_ERROR` | Error message | `"There was an error…"` | ##### AI composer [#Overrides-AI-composer] AI composer overrides can be set on both [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) and [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | ------------------------- | ------------------ | ------------------ | | `AI_COMPOSER_PLACEHOLDER` | Input placeholder | `"Ask anything…"` | | `AI_COMPOSER_SEND` | Send button label | `"Send"` | | `AI_COMPOSER_ABORT` | Abort button label | `"Abort response"` | ##### AI chat message [#Overrides-AI-chat-message] AI chat message overrides can be set on both [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) and [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | --------------------------- | ------------------- | ----------------------------------- | | `AI_CHAT_MESSAGE_DELETED` | Deleted message | `"This message has been deleted."` | | `AI_CHAT_MESSAGE_THINKING` | Thinking indicator | `"Thinking…"` | | `AI_CHAT_MESSAGE_REASONING` | Reasoning indicator | `"Reasoning…"` / `"Reasoned for X"` | | `AI_CHAT_MESSAGE_RETRIEVAL` | Retrieval indicator | `"Searching X…"` / `"Searched X"` | ##### AI chat [#Overrides-AI-chat] AI chat overrides can be set on both [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) and [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | ------------------------ | -------------- | ----------------------- | | `AI_CHAT_MESSAGES_ERROR` | Messages error | `"There was an error…"` | ##### AI tool confirmation [#Overrides-AI-tool-confirmation] AI tool confirmation overrides can be set on both [`AiTool`](/docs/api-reference/liveblocks-react-ui#AiTool) and [`LiveblocksUiConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig). | Name | Description | Default value | | ------------------------------ | -------------------- | ------------- | | `AI_TOOL_CONFIRMATION_CONFIRM` | Confirm button label | `"Confirm"` | | `AI_TOOL_CONFIRMATION_CANCEL` | Cancel button label | `"Cancel"` | --- meta: title: "@liveblocks/react" parentTitle: "API Reference" description: "API Reference for the @liveblocks/react package" alwaysShowAllNavigationLevels: false --- `@liveblocks/react` provides you with [React](https://react.dev/) bindings for our realtime collaboration APIs, built on top of WebSockets. Read our [getting started](/docs/get-started) guides to learn more. ## Suspense All Liveblocks React components and hooks can be exported from two different locations, `@liveblocks/react/suspense` and `@liveblocks/react`. This is because Liveblocks provides two types of hooks; those that support [React Suspense](https://react.dev/reference/react/Suspense), and those that don’t. ```tsx // Import the Suspense hook import { useThreads } from "@/liveblocks/react/suspense"; // Import the regular hook import { useThreads } from "@/liveblocks/react"; ``` We recommend importing from `@liveblocks/react/suspense` and using Suspense by default, as it often makes it easier to build your collaborative application. 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. The result is cached by the Liveblocks client, and called fresh only when necessary, so never cache it yourself. 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). 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). Deprecated. For new rooms, use [`engine: 2`](#RoomProvider) instead. Engine 2 rooms have native support for streaming. This flag will be removed in a future version, but will continue to work for existing engine 1 rooms for now. [Learn more](/docs/guides/the-new-storage-engine-and-its-benefits). #### 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 */} ); } ``` Never cache your authentication endpoint, as your client will not function correctly. The Liveblocks client will cache results for you, only making requests to the endpoint if necessary, such as when the token has expired. #### 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. Preferred storage engine version to use when creating the room. Only takes effect if the room doesn’t exist yet. The v2 Storage engine supports larger documents, is more performant, has native streaming support, and will become the default in the future. [Learn more](/docs/guides/about-the-new-storage-engine). #### 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 = {}; type CommentMetadata = {}; // +++ export const { RoomProvider, useMyPresence, // Other hooks // ... } = createRoomContext< Presence, Storage, UserMeta, RoomEvent, ThreadMetadata, CommentMetadata >(client); // +++ ``` #### Suspense with createRoomContext [#createRoomContext-Suspense] To use the React suspense version of our hooks with `createRoomContext`, you can export from the `suspense` property instead. ```tsx file="liveblocks.config.ts" import { createClient } from "@liveblocks/client"; import { createRoomContext } from "@liveblocks/react"; const client = createClient({ // publicApiKey: "", // authEndpoint: "/api/liveblocks-auth", }); type Presence = {}; type Storage = {}; type UserMeta = {}; type RoomEvent = {}; type ThreadMetadata = {}; type CommentMetadata = {}; export const { // +++ suspense: { RoomProvider, useMyPresence, // Other suspense hooks // ... }, // +++ } = createRoomContext< Presence, Storage, UserMeta, RoomEvent, ThreadMetadata, CommentMetadata >(client); ``` #### Typing createRoomContext To type your hooks, you can pass multiple different types to `createRoomContext`. A full explanation is in the code snippet below. ```tsx file="liveblocks.config.ts" isCollapsable isCollapsed import { createClient } from "@liveblocks/client"; import { createRoomContext } from "@liveblocks/react"; const client = createClient({ // publicApiKey: "", // authEndpoint: "/api/liveblocks-auth", }); // Presence represents the properties that exist on every user in the Room // and that will automatically be kept in sync. Accessible through the // `user.presence` property. Must be JSON-serializable. type Presence = { // cursor: { x: number, y: number } | null, // ... }; // Optionally, Storage represents the shared document that persists in the // Room, even after all users leave. Fields under Storage typically are // LiveList, LiveMap, LiveObject instances, for which updates are // automatically persisted and synced to all connected clients. type Storage = { // animals: LiveList, // ... }; // 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, disable } = useHistory(); ``` _None_ The room's history object containing methods for [`undo`](/docs/api-reference/liveblocks-client#Room.history.undo), [`redo`](/docs/api-reference/liveblocks-client#Room.history.redo), [`pause`](/docs/api-reference/liveblocks-client#Room.history.pause), [`resume`](/docs/api-reference/liveblocks-client#Room.history.resume), and [`disable`](/docs/api-reference/liveblocks-client#Room.history.disable) 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 thread and comment metadata. ```tsx import { useCreateThread } from "@liveblocks/react/suspense"; const createThread = useCreateThread(); const thread = createThread({ body: {}, attachments: [], metadata: {}, commentMetadata: {}, }); ``` A function that creates a thread with an initial comment, and optionally thread and comment 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 are resolved. 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 = searchResolved === "all" ? undefined : searchResolved === "resolved"; // +++ 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: [], metadata: {}, }); ``` 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, and optionally its attachments and metadata. ```tsx import { useEditComment } from "@liveblocks/react/suspense"; const editComment = useEditComment(); editComment({ threadId: "th_xxx", commentId: "cm_xxx", body: {}, attachments: [], metadata: {}, }); ``` A function that edits a comment’s body, and optionally its attachments and metadata. #### Error handling [#useEditComment-error-handling] `useEditComment` edits comments optimistically, meaning that the comment appears updated instantly, before Liveblocks has confirmed a successful comment edit. To catch any errors that occur, add [`useErrorListener`][] and look for the `EDIT_COMMENT_ERROR` type. ```tsx import { useErrorListener } from "@liveblocks/react/suspense"; useErrorListener((error) => { if (error.context.type === "EDIT_COMMENT_ERROR") { const { roomId, threadId, commentId, body } = error.context; console.log(`Problem editing comment ${commentId}`); } }); ``` ### useEditCommentMetadata [@badge=RoomProvider] Returns a function that edits a comment’s metadata. To delete an existing metadata property, set its value to `null`. Passing `undefined` for a metadata property will ignore it. ```tsx import { useEditCommentMetadata } from "@liveblocks/react/suspense"; const editCommentMetadata = useEditCommentMetadata(); editCommentMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: {}, }); ``` A function that edits a comment’s metadata. To delete an existing metadata property, set its value to `null`. #### Error handling [#useEditCommentMetadata-error-handling] `useEditCommentMetadata` edits comment metadata optimistically, meaning that the metadata appears updated instantly, before Liveblocks has confirmed a successful metadata update. To catch any errors that occur, add [`useErrorListener`][] and look for the `EDIT_COMMENT_METADATA_ERROR` type. ```tsx import { useErrorListener } from "@liveblocks/react/suspense"; useErrorListener((error) => { if (error.context.type === "EDIT_COMMENT_METADATA_ERROR") { const { roomId, threadId, commentId, metadata } = error.context; console.log(`Problem editing comment metadata ${commentId}`); } }); ``` ### useDeleteComment [@badge=RoomProvider] Returns a function that deletes a comment. If it is the last non-deleted comment, the thread also gets deleted. ```tsx import { useDeleteComment } from "@liveblocks/react/suspense"; const deleteComment = useDeleteComment(); deleteComment({ threadId: "th_xxx", commentId: "cm_xxx" }); ``` 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. ## Feeds ### useFeeds [@badge=RoomProvider] Returns a paginated list of feeds within the current room. Results are sorted oldest first and can be [filtered](#useFeeds-filtering) and [paginated](#useFeeds-pagination). [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 { useFeeds } from "@liveblocks/react"; const { feeds, error, isLoading } = useFeeds(); ``` Optional configuration object. Optional timestamp filter (ms). Only feeds whose `createdAt` or `updatedAt` is at or after this value are included in `feeds` (applied to the cached data for this hook’s options). Optional metadata filter. Only feeds whose metadata matches every key/value pair are included (applied to the cached data for this hook’s options). Page size for each server request when loading or loading more feeds. Does not cap how many feeds appear in `feeds`—use `fetchMore` until `hasFetchedAll` is true. An array of feeds within the current room matching `since` and `metadata` for this call, sorted by `createdAt` ascending (tie-break on `feedId`), or `undefined` if not yet loaded (in non-Suspense version). Whether the feeds are currently being loaded. Any error that occurred while loading the feeds. Whether all available feeds have been fetched. A function to fetch more feeds. Whether more feeds are currently being fetched. Any error that occurred while fetching more feeds. #### Filtering [#useFeeds-filtering] It’s possible to filter feeds by timestamp and metadata, and results are merged into a per-room cache. Limit sets the page size for each fetch when [paginating](). ```tsx const { feeds } = useFeeds({ // Optional, fetch feeds from the last day since: Date.now() - 1000 * 60 * 60 * 24, // Optional, fetch feeds with the `{ tag: "design" }` metadata metadata: { tag: "design", }, // Optional, fetch only 10 at a time limit: 10, }); ``` #### Pagination [#useFeeds-pagination] By default, the `useFeeds` hook returns up to 50 feeds. To fetch more, the hook provides additional fields for pagination, similar to [`useThreads`][]. ```tsx import { useFeeds } from "@liveblocks/react"; const { feeds, error, isLoading, // +++ fetchMore, isFetchingMore, hasFetchedAll, fetchMoreError, // +++ } = useFeeds(); ``` - `hasFetchedAll` indicates whether all available feeds have been fetched. - `fetchMore` loads up to 50 more feeds, and is always safe to call. - `isFetchingMore` indicates whether more feeds are being fetched. - `fetchMoreError` returns error statuses resulting from fetching more. ##### Pagination example [#useFeeds-pagination-example] The following example demonstrates how to use the `fetchMore` function to implement a “Load More” button, which fetches additional feeds when clicked. The button is disabled while fetching is in progress. ```tsx import { Feed } from "@liveblocks/react-ui"; import { useFeeds } from "@liveblocks/react/suspense"; function Feeds() { const { feeds, hasFetchedAll, fetchMore, isFetchingMore } = useFeeds(); return (
{feeds.map((feed) => ( {feed.metadata.name} ))} // +++ {hasFetchedAll ? (
🎉 You've loaded all feeds!
) : ( )} // +++
); } ``` ### useFeedMessages [@badge=RoomProvider] Returns a paginated list of messages for a specific feed in the current room. Messages are sorted newest first using their `createdAt` property and can be [paginated](#useFeedMessages-pagination). [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 { useFeedMessages } from "@liveblocks/react"; const { messages, error, isLoading } = useFeedMessages("my-feed-id"); ``` The ID of the feed to get messages from. Optional configuration object. Optional cursor for pagination. Page size for each server request when loading or loading more messages. Does not cap how many messages appear in `messages`—use `fetchMore` until `hasFetchedAll` is true. An array of messages in the feed (chronological order by `createdAt`), or `undefined` if not yet loaded (in non-Suspense version). Whether the messages are currently being loaded. Any error that occurred while loading the messages. Whether all available messages have been fetched. A function to fetch more messages. Whether more messages are currently being fetched. Any error that occurred while fetching more messages. #### Pagination [#useFeedMessages-pagination] By default, the `useFeedMessages` hook returns up to 50 messages. To fetch more, the hook provides additional fields for pagination, similar to [`useThreads`][]. ```tsx import { useFeedMessages } from "@liveblocks/react"; const { messages, error, isLoading, // +++ hasFetchedAll, fetchMore, isFetchingMore, fetchMoreError, // +++ } = useFeedMessages("my-feed-id"); ``` - `hasFetchedAll` indicates whether all available messages have been fetched. - `fetchMore` loads up to 50 more messages, and is always safe to call. - `isFetchingMore` indicates whether more messages are being fetched. - `fetchMoreError` returns error statuses resulting from fetching more. ##### Pagination example [#useFeedMessages-pagination-example] The following example demonstrates how to use the `fetchMore` function to implement a “Load More” button, which fetches additional messages when clicked. The button is disabled while fetching is in progress. ```tsx import { useFeedMessages } from "@liveblocks/react/suspense"; function FeedMessages({ feedId }: { feedId: string }) { const { messages, hasFetchedAll, fetchMore, isFetchingMore } = useFeedMessages(feedId); return (
{messages.map((message) => (
{message.data.content}
))} // +++ {hasFetchedAll ? (
🎉 You've loaded all messages!
) : ( )} // +++
); } ``` ### useCreateFeed [@badge=RoomProvider] Returns a function that creates a new feed in the current room. ```tsx import { useCreateFeed } from "@liveblocks/react"; const createFeed = useCreateFeed(); createFeed("my-feed-id", { metadata: { name: "My Feed", channel: true }, timestamp: Date.now(), }); ``` A function that creates a feed. Takes the feed ID and optional metadata and timestamp. ### useDeleteFeed [@badge=RoomProvider] Returns a function that deletes a feed from the current room. ```tsx import { useDeleteFeed } from "@liveblocks/react"; const deleteFeed = useDeleteFeed(); deleteFeed("my-feed-id"); ``` A function that deletes a feed. Takes the feed ID. ### useUpdateFeedMetadata [@badge=RoomProvider] Returns a function that updates a feed's metadata in the current room. ```tsx import { useUpdateFeedMetadata } from "@liveblocks/react"; const updateFeedMetadata = useUpdateFeedMetadata(); updateFeedMetadata("my-feed-id", { name: "Updated Name", updated: new Date().toISOString(), }); ``` A function that updates a feed's metadata. Takes the feed ID and the new metadata object. ### useCreateFeedMessage [@badge=RoomProvider] Returns a function that adds a new message to a feed in the current room. ```tsx import { useCreateFeedMessage } from "@liveblocks/react"; const createFeedMessage = useCreateFeedMessage(); createFeedMessage("my-feed-id", { role: "user", content: "Hello, world!" }); // With optional id and timestamp createFeedMessage( "my-feed-id", { role: "user", content: "Hello!" }, { id: "my-message-id", timestamp: Date.now(), } ); ``` A function that adds a message to a feed. Takes the feed ID, message data, and optional id and timestamp. ### useDeleteFeedMessage [@badge=RoomProvider] Returns a function that deletes a message from a feed in the current room. ```tsx import { useDeleteFeedMessage } from "@liveblocks/react"; const deleteFeedMessage = useDeleteFeedMessage(); deleteFeedMessage("my-feed-id", "my-message-id"); ``` A function that deletes a feed message. Takes the feed ID and message ID. ### useUpdateFeedMessage [@badge=RoomProvider] Returns a function that updates a feed message in the current room. ```tsx import { useUpdateFeedMessage } from "@liveblocks/react"; const updateFeedMessage = useUpdateFeedMessage(); updateFeedMessage("my-feed-id", "my-message-id", { role: "user", content: "Updated content", }); ``` A function that updates a feed message. Takes the feed ID, message ID, and the new data object. #### Typing feed metadata and message data [#feeds-typescript] You can type feed metadata and feed message data using the `Liveblocks` interface in your `liveblocks.config.ts` file. This provides type safety for [`useFeeds`](#useFeeds) and [`useFeedMessages`](#useFeedMessages). ```ts // liveblocks.config.ts declare global { interface Liveblocks { FeedMetadata: { name?: string; channel?: boolean; agentName?: string; }; FeedMessageData: { role: "user" | "assistant" | "system"; content: string; }; } } ``` ## 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. Preferred storage engine version to use when creating the room. Only takes effect if the room doesn't exist yet. ### 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. Preferred storage engine version to use when creating the room. Only takes effect if the room doesn't exist yet. 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 trackEvent="install_liveblocks" 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 a CodeMirror code editor using Liveblocks and JavaScript" parentTitle: "Quickstart" description: "Learn how to install a CodeMirror code editor using Liveblocks and JavaScript" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your JavaScript application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and CodeMirror Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" npm install @liveblocks/client @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next ``` 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 a CodeMirror code editor using Liveblocks and React" parentTitle: "Quickstart" description: "Learn how to install a CodeMirror code editor using Liveblocks and React" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your React application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and CodeMirror Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next ``` 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 a CodeMirror code editor using Liveblocks and Svelte" parentTitle: "Quickstart" description: "Learn how to install a CodeMirror code editor using Liveblocks and Svelte" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your Svelte application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and CodeMirror Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" npm install @liveblocks/client @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next ``` 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 a CodeMirror code editor using Liveblocks and Vue.js" parentTitle: "Quickstart" description: "Learn how to install a CodeMirror code editor using Liveblocks and Vue.js" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your Vue.js application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and CodeMirror Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" npm install @liveblocks/client @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next ``` 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 a Monaco code editor using Liveblocks and JavaScript" parentTitle: "Quickstart" description: "Learn how to install a Monaco code editor using Liveblocks and JavaScript" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your JavaScript application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Monaco Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" 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 a Monaco code editor using Liveblocks and React" parentTitle: "Quickstart" description: "Learn how to install a Monaco code editor using Liveblocks and React" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your React application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Monaco Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs @monaco-editor/react y-monaco y-protocols ``` 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 a Monaco code editor using Liveblocks and Svelte" parentTitle: "Quickstart" description: "Learn how to install a Monaco code editor using Liveblocks and Svelte" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your Svelte application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Monaco Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" 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 a Monaco code editor using Liveblocks and Vue.js" parentTitle: "Quickstart" description: "Learn how to install a Monaco code editor using Liveblocks and Vue.js" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your Vue.js application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Monaco Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" 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 a Quill text editor using Liveblocks and JavaScript" parentTitle: "Quickstart" description: "Learn how to install a Quill text editor using Liveblocks and JavaScript" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your JavaScript application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Quill Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" 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 a Quill text editor using Liveblocks and React" parentTitle: "Quickstart" description: "Learn how to install a Quill text editor using Liveblocks and React" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your React application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Quill Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs quill quill-cursors react-quill y-quill ``` 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 a Quill text editor using Liveblocks and Svelte" parentTitle: "Quickstart" description: "Learn how to install a Quill text editor using Liveblocks and Svelte" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your Svelte application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Quill Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" 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 a Quill text editor using Liveblocks and Vue.js" parentTitle: "Quickstart" description: "Learn how to install a Quill text editor using Liveblocks and Vue.js" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your Vue.js application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Quill Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" 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 a Slate text editor using Liveblocks and React" parentTitle: "Quickstart" description: "Learn how to install a Slate text editor using Liveblocks and React" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your React application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Slate Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs slate slate-react @slate-yjs/core ``` 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 a Tiptap text editor using Liveblocks and JavaScript" parentTitle: "Quickstart" description: "Learn how to install a Tiptap text editor using Liveblocks and JavaScript" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your JavaScript application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Tiptap Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" npm install @liveblocks/client @liveblocks/yjs yjs @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor y-prosemirror ``` 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 a Tiptap text editor using Liveblocks and Svelte" parentTitle: "Quickstart" description: "Learn how to install a Tiptap text editor using Liveblocks and Svelte" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your Svelte application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Tiptap Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" npm install @liveblocks/client @liveblocks/yjs yjs @tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor y-prosemirror ``` 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 a Tiptap text editor using Liveblocks and Vue.js" parentTitle: "Quickstart" description: "Learn how to install a Tiptap text editor using Liveblocks and Vue.js" --- Liveblocks is a realtime collaboration infrastructure for building performant collaborative experiences. Follow the following steps to start adding collaboration to your Vue.js application using the APIs from the [`@liveblocks/yjs`](/docs/api-reference/liveblocks-yjs) package. ## Quickstart Install Liveblocks, Yjs, and Tiptap Every Liveblocks package should use the same version. ```bash trackEvent="install_liveblocks" npm install @liveblocks/client @liveblocks/yjs yjs @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor y-prosemirror ``` 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 --- ## Collaboration features ## API Reference } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ## Examples ## Community } openInNewWindow /> } openInNewWindow /> } openInNewWindow /> --- meta: title: "Provider name" parentTitle: "Integrations" description: "Use Provider with Liveblocks to..." --- # Provider page template Use this file as the starting point for new integration provider pages. Do not add this file to `docs/routes.json`; copy it to `docs/pages/integrations/{slug}.mdx` and route that page when the provider is ready to publish. ## Route entry Add the published provider to the Integrations category in `docs/routes.json`. Keep the route visible unless the page must be generated without appearing in navigation. ```json { "title": "Provider name", "path": "/integrations/provider-slug", "keywords": [ "Provider name", "provider-slug", "integration", "category", "common product term" ] } ``` ## Metadata Use the same frontmatter shape on every provider page. ```mdx --- meta: title: "Provider name" parentTitle: "Integrations" description: "Use Provider with Liveblocks to add realtime collaboration to..." --- ``` ## Required sections Each provider page should include these headings so the page is predictable, searchable, and easy to review. ## What it enables Explain the integration outcome in product terms. Name the Liveblocks features the provider unlocks or complements, and say whether the page is a ready-to-use integration, an implementation pattern, a marketplace listing, or partner submission prep. ## When to use Liveblocks with Provider Describe the best-fit use cases and the boundary between Liveblocks and the provider. ## Recommended architecture Show which system owns authentication, durable data, realtime collaboration, workflow automation, deployment, or generated code. Use a table when the split is important. If the page needs a diagram, use a Mermaid code fence. Keep labels short and describe system boundaries, not every implementation detail. ```mermaid graph TD A[User action] --> B[Liveblocks feature] B --> C[Webhook or REST API] C --> D[Your app endpoint] D --> E[Provider API or database] ``` ## Setup Use `Steps`, `Step`, `StepTitle`, and `StepContent` for implementation steps. Include environment variables, install commands, auth setup, provider setup, and the first Liveblocks feature to add. ## Examples and templates Link to runnable examples, starter kits, partner templates, marketplace entries, or closest existing Liveblocks examples. ## Limitations and troubleshooting Document known constraints, rate limits, eventual consistency, permissions, secret handling, provider-specific gotchas, and when the provider should not be treated as the Liveblocks source of truth. ## Partner listing notes Record marketplace submission copy, category recommendations, listing URLs, owner notes, and any provider-specific requirements. ## Related docs Link to the most relevant Liveblocks docs, such as Authentication, REST API, Webhooks, Comments, Notifications, Multiplayer, AI Copilots, MCP server, or n8n nodes. --- meta: title: "Bolt + Liveblocks" parentTitle: "Integrations" description: "Add Liveblocks to apps built or exported from Bolt so you can ship realtime presence, multiplayer editors, and comments alongside generated UI." --- [Bolt](https://bolt.new/) helps you generate full-stack apps quickly using AI chats. With Liveblocks, you can add realtime collaboration to your generated Bolt app, such as [collaborative text editing](/docs/collaboration-features/multiplayer/text-editor), [multiplayer documents](/docs/collaboration-features/multiplayer/sync-engine), [comment threads](/docs/collaboration-features/comments), [inbox notifications](/docs/collaboration-features/notifications), and [AI copilots](/docs/collaboration-features/ai-copilots). ## Setup Ask the chat to build with Liveblocks Add Liveblocks to your project by asking the chat to add collaborative features, for example: ```text title="Example prompt" Add a realtime text editor with Liveblocks and Tiptap. ``` To find more ways to use Liveblocks, visit our [showcase](/showcase) and read our [get started guides](/docs/get-started). Insert your secret key During the process, Bolt will prompt you to insert your Liveblocks secret key from the [dashboard](/dashboard/apikeys). Paste in your project’s key. ```text title="Secret key" {{SECRET_KEY}} ``` To manually edit your key in Bolt, go to Settings → Secrets → `LIVEBLOCKS_SECRET_KEY`. Collaboration is ready Your app should now have live collaboration—use the chat to continue building your Liveblocks app. ## Limitations and troubleshooting ### Secret or public key issues To fix issues related to API keys, instruct Bolt to use a secret key instead of a public key, and authenticate your Liveblocks application. --- meta: title: "Claude Code + Liveblocks" parentTitle: "Integrations" description: "Use Claude Code with Liveblocks agent skills and the MCP server to build multiplayer apps with guided best practices." --- Use [Claude Code](https://www.anthropic.com/claude-code) with Liveblocks to add collaborative features to your app. Install [agent skills](/docs/tools/agent-skills) so your assistant follows Liveblocks and Yjs guidance, and use our [MCP server](/docs/tools/mcp-server) to allow AI to inspect and modify your data, such as rooms, Storage, Yjs, comments, and more. ## Setup Install agent skills To help AI follow best practices in your app, add [Liveblocks agent skills](/docs/tools/agent-skills) to your system. Make sure to select Claude Code in the CLI. Global installation is easiest. ```bash npx skills add liveblocks/skills ``` Install MCP server To allow AI to inspect and modify data in your project, install the [Liveblocks MCP server](/docs/tools/mcp-server). Run the following command in the terminal, inserting your secret key from [your dashboard](/dashboard): ```bash claude mcp add liveblocks -e LIVEBLOCKS_SECRET_KEY="{{SECRET_KEY}}" -- npx -y github:liveblocks/liveblocks-mcp-server ``` Do not use a secret key from a production project, as AI will have direct access to delete data. Get help from your AI assistant You can now use your AI assistant to help you structure and debug your Liveblocks application. Here are a few examples. > Q: Set up a Liveblocks room on this page. > > A: _Certainly, I've set up a Liveblocks room on this page using `RoomProvider`, it has…_
> Q: How many rooms are there? > > A: _There are 10 rooms, the last was created 7 minutes ago. Its room ID is `l8Gaj9` and…_
> Q: Add placeholder comments to the `l8Gaj9` room. > > A: _I've filled the room with example commands and threads, following your instructions._
## More information ### Agent skills Two agent skills are available to your assistant, `liveblocks-best-practices` and `yjs-best-practices`. These are collections of markdown files that detail various best practices for using Liveblocks and Yjs. We always recommend using these skills when building, debugging, or answering questions about Liveblocks and Yjs. ### MCP server Most Liveblocks [REST API](/docs/api-reference/rest-api-endpoints) operations as tools, so your AI can inspect and edit rooms and data from the editor. This is helpful for fetching data when debugging, and also useful for creating placeholder content in your development app, for example you can ask AI to create new rooms and add placeholder comments and data. Each Liveblocks project has a unique secret key, which you pass when installing the MCP server. Only data from this project can be inspected. Never use a secret key from a production project, as giving AI direct access would be dangerous, allowing it to delete your production data. ## Limitations and troubleshooting ### Skills do not load Re-run `npx skills add liveblocks/skills` from the repo root. Ensure Claude Code can see the generated plugin files. ### MCP returns auth errors Use a secret key from the same [Liveblocks project](/dashboard) as your app. Confirm `LIVEBLOCKS_SECRET_KEY` in the MCP config matches that project. ### MCP returns incorrect data Check that you’re using the correct secret key for your current project. Try uninstalling and reinstalling with the correct key. ## Related docs - [Agent skills](/docs/tools/agent-skills). - [MCP server](/docs/tools/mcp-server). - [REST API reference](/docs/api-reference/rest-api-endpoints). --- meta: title: "Claude Desktop + Liveblocks" parentTitle: "Integrations" description: "Use Claude Desktop with Liveblocks agent skills and the MCP server to inspect and modify your project with AI." --- Use [Claude Desktop](https://claude.ai/download) with Liveblocks to add collaborative features to your app. Install [agent skills](/docs/tools/agent-skills) so your assistant follows Liveblocks and Yjs guidance, and use our [MCP server](/docs/tools/mcp-server) to allow AI to inspect and modify your data, such as rooms, Storage, Yjs, comments, and more. ## Setup Install agent skills To help AI follow best practices in your app, add [Liveblocks agent skills](/docs/tools/agent-skills) to your system. Make sure to select Claude Desktop in the CLI. Global installation is easiest. ```bash npx skills add liveblocks/skills ``` Install MCP server To allow AI to inspect and modify data in your project, install the [Liveblocks MCP server](/docs/tools/mcp-server). 1. In Claude Desktop, go to Settings → Developer → Edit Config. 2. Open the JSON file, `claude_desktop_config.json`. 3. Add the following JSON, inserting your secret key from [your dashboard](/dashboard): ```json { "mcpServers": { "liveblocks": { "command": "npx", "args": ["-y", "github:liveblocks/liveblocks-mcp-server"], "env": { "LIVEBLOCKS_SECRET_KEY": "{{SECRET_KEY}}" } } } } ``` Do not use a secret key from a production project, as AI will have direct access to delete data. Get help from your AI assistant You can now use your AI assistant to help you structure and debug your Liveblocks application. Here are a few examples. > Q: Set up a Liveblocks room on this page. > > A: _Certainly, I've set up a Liveblocks room on this page using `RoomProvider`, it has…_
> Q: How many rooms are there? > > A: _There are 10 rooms, the last was created 7 minutes ago. Its room ID is `l8Gaj9` and…_
> Q: Add placeholder comments to the `l8Gaj9` room. > > A: _I've filled the room with example commands and threads, following your instructions._
## More information ### Agent skills Two agent skills are available to your assistant, `liveblocks-best-practices` and `yjs-best-practices`. These are collections of markdown files that detail various best practices for using Liveblocks and Yjs. We always recommend using these skills when building, debugging, or answering questions about Liveblocks and Yjs. ### MCP server Most Liveblocks [REST API](/docs/api-reference/rest-api-endpoints) operations as tools, so your AI can inspect and edit rooms and data from Claude Desktop. This is helpful for fetching data when debugging, and also useful for creating placeholder content in your development app, for example you can ask AI to create new rooms and add placeholder comments and data. Each Liveblocks project has a unique secret key, which you pass when installing the MCP server. Only data from this project can be inspected. Never use a secret key from a production project, as giving AI direct access would be dangerous, allowing it to delete your production data. ## Limitations and troubleshooting ### Skills do not load Re-run `npx skills add liveblocks/skills` and ensure Claude Desktop is selected in the CLI. Restart Claude Desktop after installation. ### MCP returns auth errors Use a secret key from the same [Liveblocks project](/dashboard) as your app. Confirm `LIVEBLOCKS_SECRET_KEY` in `claude_desktop_config.json` matches that project. ### MCP returns incorrect data Check that you’re using the correct secret key for your current project. Try uninstalling and reinstalling with the correct key. ## Related docs - [Agent skills](/docs/tools/agent-skills). - [MCP server](/docs/tools/mcp-server). - [REST API reference](/docs/api-reference/rest-api-endpoints). --- meta: title: "OpenAI Codex + Liveblocks" parentTitle: "Integrations" description: "Use OpenAI Codex with Liveblocks agent skills and the MCP server to build collaborative apps with guided best practices." --- Use [OpenAI Codex](https://openai.com/codex/) with Liveblocks to add collaborative features to your app. Install [agent skills](/docs/tools/agent-skills) so your assistant follows Liveblocks and Yjs guidance, and use our [MCP server](/docs/tools/mcp-server) to allow AI to inspect and modify your data, such as rooms, Storage, Yjs, comments, and more. ## Setup Install agent skills To help AI follow best practices in your app, add [Liveblocks agent skills](/docs/tools/agent-skills) to your system. Make sure to select Codex in the CLI. Global installation is easiest. ```bash npx skills add liveblocks/skills ``` Install MCP server To allow AI to inspect and modify data in your project, first ensure the Codex CLI is installed: ```bash npm i -g @openai/codex ``` Next, install the [Liveblocks MCP server](/docs/tools/mcp-server), inserting your secret key from [your dashboard](/dashboard): ```bash codex mcp add liveblocks \ --env LIVEBLOCKS_SECRET_KEY="{{SECRET_KEY}}" \ -- npx -y github:liveblocks/liveblocks-mcp-server ``` Do not use a secret key from a production project, as AI will have direct access to delete data. Get help from your AI assistant You can now use your AI assistant to help you structure and debug your Liveblocks application. Here are a few examples. > Q: Set up a Liveblocks room on this page. > > A: _Certainly, I've set up a Liveblocks room on this page using `RoomProvider`, it has…_
> Q: How many rooms are there? > > A: _There are 10 rooms, the last was created 7 minutes ago. Its room ID is `l8Gaj9` and…_
> Q: Add placeholder comments to the `l8Gaj9` room. > > A: _I've filled the room with example commands and threads, following your instructions._
## More information ### Agent skills Two agent skills are available to your assistant, `liveblocks-best-practices` and `yjs-best-practices`. These are collections of markdown files that detail various best practices for using Liveblocks and Yjs. We always recommend using these skills when building, debugging, or answering questions about Liveblocks and Yjs. ### MCP server Most Liveblocks [REST API](/docs/api-reference/rest-api-endpoints) operations as tools, so your AI can inspect and edit rooms and data from the editor. This is helpful for fetching data when debugging, and also useful for creating placeholder content in your development app, for example you can ask AI to create new rooms and add placeholder comments and data. Each Liveblocks project has a unique secret key, which you pass when installing the MCP server. Only data from this project can be inspected. Never use a secret key from a production project, as giving AI direct access would be dangerous, allowing it to delete your production data. ## Limitations and troubleshooting ### Skills do not load Re-run `npx skills add liveblocks/skills` from the repo root. Ensure Codex can see the generated plugin files. ### MCP returns auth errors Use a secret key from the same [Liveblocks project](/dashboard) as your app. Confirm `LIVEBLOCKS_SECRET_KEY` in the MCP config matches that project. ### MCP returns incorrect data Check that you’re using the correct secret key for your current project. Try uninstalling and reinstalling with the correct key. ## Related docs - [Agent skills](/docs/tools/agent-skills). - [MCP server](/docs/tools/mcp-server). - [REST API reference](/docs/api-reference/rest-api-endpoints). --- meta: title: "Cursor + Liveblocks" parentTitle: "Integrations" description: "Use Cursor with Liveblocks agent skills and the MCP server to build multiplayer apps with guided best practices." --- Use [Cursor](https://cursor.com/) with Liveblocks to add collaborative features to your app. Install [agent skills](/docs/tools/agent-skills) so your assistant follows Liveblocks and Yjs guidance, and use our [MCP server](/docs/tools/mcp-server) to allow AI to inspect and modify your data, such as rooms, Storage, Yjs, comments, and more. ## Setup Install agent skills To help AI follow best practices in your app, add [Liveblocks agent skills](/docs/tools/agent-skills) to your system. Make sure to select Cursor in the CLI. Global installation is easiest. ```bash npx skills add liveblocks/skills ``` Install MCP server To allow AI to inspect and modify data in your project, install the [Liveblocks MCP server](/docs/tools/mcp-server). 1. Go to File → Cursor Settings → MCP → Add new server. 2. Add the following JSON, inserting your secret key from [your dashboard](/dashboard): ```json { "mcpServers": { "liveblocks": { "command": "npx", "args": ["-y", "github:liveblocks/liveblocks-mcp-server"], "env": { "LIVEBLOCKS_SECRET_KEY": "{{SECRET_KEY}}" } } } } ``` Do not use a secret key from a production project, as AI will have direct access to delete data. Get help from your AI assistant You can now use your AI assistant to help you structure and debug your Liveblocks application. Here are a few examples. > Q: Set up a Liveblocks room on this page. > > A: _Certainly, I've set up a Liveblocks room on this page using `RoomProvider`, it has…_
> Q: How many rooms are there? > > A: _There are 10 rooms, the last was created 7 minutes ago. Its room ID is `l8Gaj9` and…_
> Q: Add placeholder comments to the `l8Gaj9` room. > > A: _I've filled the room with example commands and threads, following your instructions._
## More information ### Agent skills Two agent skills are available to your assistant, `liveblocks-best-practices` and `yjs-best-practices`. These are collections of markdown files that detail various best practices for using Liveblocks and Yjs. We always recommend using these skills when building, debugging, or answering questions about Liveblocks and Yjs. ### MCP server Most Liveblocks [REST API](/docs/api-reference/rest-api-endpoints) operations as tools, so your AI can inspect and edit rooms and data from the editor. This is helpful for fetching data when debugging, and also useful for creating placeholder content in your development app, for example you can ask AI to create new rooms and add placeholder comments and data. Each Liveblocks project has a unique secret key, which you pass when installing the MCP server. Only data from this project can be inspected. Never use a secret key from a production project, as giving AI direct access would be dangerous, allowing it to delete your production data. ## Limitations and troubleshooting ### Skills do not load Re-run `npx skills add liveblocks/skills` from the repo root. Ensure Cursor can see the generated plugin files. ### MCP returns auth errors Use a secret key from the same [Liveblocks project](/dashboard) as your app. Confirm `LIVEBLOCKS_SECRET_KEY` in the MCP config matches that project. ### MCP returns incorrect data Check that you’re using the correct secret key for your current project. Try uninstalling and reinstalling with the correct key. ## Related docs - [Agent skills](/docs/tools/agent-skills). - [MCP server](/docs/tools/mcp-server). - [REST API reference](/docs/api-reference/rest-api-endpoints). --- meta: title: "Lovable + Liveblocks" parentTitle: "Integrations" description: "Add Liveblocks to apps built or exported from Lovable so you can ship realtime presence, multiplayer editors, and comments alongside generated UI." --- [Lovable](https://lovable.dev/) helps you generate full-stack apps quickly using AI chats. With Liveblocks, you can add realtime collaboration to your generated Lovable app, such as [collaborative text editing](/docs/collaboration-features/multiplayer/text-editor), [multiplayer documents](/docs/collaboration-features/multiplayer/sync-engine), [comment threads](/docs/collaboration-features/comments), [inbox notifications](/docs/collaboration-features/notifications), and [AI copilots](/docs/collaboration-features/ai-copilots). ## Setup Ask the chat to build with Liveblocks Add Liveblocks to your project by asking the chat to add collaborative features, for example: ```text title="Example prompt" Add a realtime text editor with Liveblocks and Tiptap. ``` To find more ways to use Liveblocks, visit our [showcase](/showcase) and read our [get started guides](/docs/get-started). Insert your secret key During the process, Lovable will prompt you to insert your Liveblocks secret key from the [dashboard](/dashboard/apikeys). Paste in your project’s key. ```text title="Secret key" {{SECRET_KEY}} ``` To manually edit your key in Lovable, go to Cloud → Secrets → `LIVEBLOCKS_SECRET_KEY`. Collaboration is ready Your app should now have live collaboration—use the chat to continue building your Liveblocks app. ## Limitations and troubleshooting ### Secret or public key issues To fix issues related to API keys, instruct Lovable to use a secret key instead of a public key, and authenticate your Liveblocks application. --- meta: title: "n8n + Liveblocks" parentTitle: "Integrations" description: "Use n8n with Liveblocks to call REST API operations and start workflows from webhook events." --- n8n is a workflow automation platform. Use n8n with Liveblocks to call the [REST API](/docs/api-reference/rest-api-endpoints) from a workflow, or start a workflow when Liveblocks sends a [webhook](/docs/platform/webhooks).
## Install Install our nodes as a [community node](https://docs.n8n.io/integrations/community-nodes/installation/) in your n8n instance. 1. Open n8n and go to **Settings** → **Community nodes**. 2. Select **Install** and enter `n8n-nodes-liveblocks` as the npm package name. 3. Check **☑︎ I understand…** and click **Install**. _Liveblocks_ and _Liveblocks Trigger_ will now appear in the node palette.
## Start with a webhook trigger The most common workflow starts when something happens in Liveblocks, for example when a comment is created. 1. Add **Liveblocks Trigger** to your n8n workflow. 2. Add your **Liveblocks Webhook Signing Secret API** credential. 3. Copy the trigger's webhook URL. 4. In the [Liveblocks dashboard](/dashboard), create a webhook endpoint with that URL. 5. Subscribe to [`commentCreated`](/docs/platform/webhooks#CommentCreatedEvent), then run the n8n workflow. When a user creates a comment, Liveblocks sends a webhook to n8n and the workflow starts. Add more n8n nodes after the trigger to send messages, create tickets, update databases, or call another Liveblocks operation. ## Operations The package provides two nodes. ### Liveblocks (action node) Calls the [Liveblocks REST API](/docs/api-reference/rest-api-endpoints). Choose a **resource**, then an **operation**. Operations map to the API. | Resource | What you can do (examples) | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Room** | List, create, update, delete, and upsert rooms; change room or organization IDs; prewarm; list active users; **set presence**; **broadcast** custom events to the room. | | **Storage** | Get, initialize, patch (JSON Patch), or delete Liveblocks storage documents. | | **Yjs** | Read Yjs state, send binary updates, list versions, and create or fetch specific versions. | | **Thread** | CRUD threads; edit metadata; resolve/unresolve; subscribe/unsubscribe; list subscriptions and thread inbox notifications. | | **Comment** | Create, read, edit, and delete comments; add/remove reactions; edit metadata. | | **Attachment** | Download comment attachments (binary response when applicable). | | **User** | Identify users for Liveblocks features that require user metadata. | | **Inbox** | Inbox notifications, notification settings, room subscription settings, mark-as-read, and related triggers. | | **Group** | Manage groups and membership; list a user’s groups. | | **AI Copilot** | List, create, read, update, and delete AI copilot configurations. | | **AI Knowledge** | Manage web and file knowledge sources, fetch markdown or links, and delete sources. | The node builds requests from the fields you configure in the UI; some endpoints accept raw JSON for advanced bodies (for example JSON Patch or broadcast payloads). Refer to the [REST API reference](/docs/api-reference/rest-api-endpoints) for request and response shapes. ### Liveblocks Trigger (webhook) Starts a workflow when Liveblocks sends a [webhook](/docs/platform/webhooks) to n8n. Configure the webhook URL from the trigger in your [Liveblocks dashboard](/dashboard) webhook settings. You can filter by **event type** or leave the filter empty to receive all supported types. Supported event filters include [`commentCreated`](/docs/platform/webhooks#CommentCreatedEvent), [`commentDeleted`](/docs/platform/webhooks#CommentDeletedEvent), [`commentEdited`](/docs/platform/webhooks#CommentEditedEvent), `notification`, [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent), [`threadCreated`](/docs/platform/webhooks#ThreadCreatedEvent), [`threadDeleted`](/docs/platform/webhooks#ThreadDeletedEvent), [`threadMetadataUpdated`](/docs/platform/webhooks#ThreadMetadataUpdatedEvent), [`userEntered`](/docs/platform/webhooks#UserEnteredEvent), [`userLeft`](/docs/platform/webhooks#UserLeftEvent), and [`ydocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent). The trigger verifies the request using your **webhook secret** before running the workflow. ## Credentials ### Liveblocks API (required for the Liveblocks action node) 1. Create or open a project in the [Liveblocks dashboard](/dashboard). 2. Copy the **secret key** (`sk_…`) from project settings. 3. In n8n, create a credential of type **Liveblocks API** and paste the secret key. The credential test calls the REST API (for example listing rooms) to confirm the key works. ### Liveblocks Webhook Secret (required for Liveblocks Trigger) 1. In the Liveblocks dashboard, open your project’s **webhook** configuration and copy the **webhook secret** (`whsec_…`). 2. In n8n, create a credential of type **Liveblocks Webhook Signing Secret API** and paste that value. This secret is only used to verify incoming webhook signatures; it is not validated with a live HTTP test in the credentials UI. ## Compatibility - This package declares **`n8nNodesApiVersion` 1** in `package.json`, in line with current n8n community node conventions. - It lists **`n8n-workflow`** as a peer dependency (version resolved by your n8n install). Use a [current n8n release](https://docs.n8n.io/release-notes/) that supports community nodes and API version 1. If you hit a compatibility issue, report it with your n8n version and this package version. ## Usage ### Call a Liveblocks operation Add **Liveblocks**, select a **resource** and **operation**, then fill in the parameters. Map data from previous nodes into room IDs, user IDs, and request bodies as needed. Use **Execute step** while designing to inspect API responses and errors. ### Start from a Liveblocks event Add **Liveblocks Trigger**, set credentials, copy the **webhook URL** into the [Liveblocks dashboard](/dashboard), and choose which events to listen for. Use n8n's test URL while building, then switch to the production URL when the workflow is active. ## Troubleshooting ### API credential fails Check that the credential uses a Liveblocks secret key that starts with `sk_`. Use the key from the same project whose rooms, threads, or notifications the workflow should access. ### Trigger does not run Check that the webhook endpoint in the [Liveblocks dashboard](/dashboard) uses the n8n trigger URL. If you are testing, use the test URL while the workflow is listening. For active workflows, use the production URL. ### Webhook verification fails Check that the trigger credential uses the webhook secret that starts with `whsec_`. This is different from your Liveblocks API secret key. ### API operation fails Failed API calls surface as n8n errors with HTTP status and message when the API returns them. Check the selected resource, operation, room ID, user ID, and JSON body against the [REST API reference](/docs/api-reference/rest-api-endpoints). New to n8n? See [Try it out](https://docs.n8n.io/try-it-out/) in the n8n docs. ## Resources - [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes). - [REST API reference](/docs/api-reference/rest-api-endpoints). - [Webhooks](/docs/platform/webhooks). - [How to test webhooks on localhost](/docs/guides/how-to-test-webhooks-on-localhost). ## Source code The source code is available in our [GitHub repository](https://github.com/liveblocks/n8n-nodes-liveblocks). --- meta: title: "Neon + Liveblocks" parentTitle: "Integrations" description: "Use Neon with Liveblocks when your collaborative app needs to store data in a Postgres database—for example mirrored collaboration data for reporting, search, audit logs, and workflows." --- [Neon](https://neon.tech/) provides serverless Postgres with branching and autoscaling. Using webhooks, you can set up one-way synchronization of your Liveblocks data to Neon for reporting, search, audit logs, or app workflows. ## How data sync works Liveblocks [webhooks](/docs/platform/webhooks) trigger when certain events happen, such as when a collaborative document updates. Liveblocks can trigger an endpoint in your back end, and from here, you can fetch the latest data and write it to Neon. Here’s an example of how it works with [Liveblocks Storage](/docs/collaboration-features/multiplayer/sync-engine/liveblocks-storage). ```mermaid sequenceDiagram actor User participant LB as Liveblocks app participant Webhook as Webhook endpoint participant Neon User->>LB: Edits document Note over LB: Throttle LB->>Webhook: **storageUpdated** webhook Webhook->>LB: **getStorageDocument** API LB-->>Webhook: Storage data Webhook->>Neon: Upsert row data Neon-->>Webhook: 200 OK ``` ## Which data can be synced? Various types of Liveblocks data can be synched to Neon with webhooks. | Name | Description | Relevant webhook | Relevant API | | ------------------------ | ------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | Rooms | Created rooms and metadata. | [`roomUpdated`](/docs/platform/webhooks#RoomUpdatedEvent) | [`getRoom`](/docs/api-reference/liveblocks-node#get-rooms-roomId) | | Active users | Currently connected users. | [`userEntered`](/docs/platform/webhooks#UserEnteredEvent) | [`getActiveUsers`](/docs/api-reference/liveblocks-node#get-active-users) | | Liveblocks Storage | Custom realtime document state. | [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent) | [`getStorageDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-storage) | | React Flow | Flowchart state. | [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent) | [`mutateFlow`](/docs/api-reference/liveblocks-react-flow#mutateFlow) | | Yjs | `Y.Doc` document state. | [`ydocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent) | [`getYjsDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc) | | Tiptap/BlockNote/Lexical | Text editor state. | [`ydocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent) | [`getYjsDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc) | | Threads | Comments, reactions, more. | [`threadCreated`](/docs/platform/webhooks#ThreadCreatedEvent) | [`getThread`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId) | This is not an exhaustive list—around [15 related webhook events](/docs/platform/webhooks#Liveblocks-events) are available, along with many [Node.js methods](/docs/api-reference/liveblocks-node), [Python functions](/docs/api-reference/liveblocks-python), and [REST API endpoints](/docs/api-reference/rest-api-endpoints). ## Setup Quickstart for synching Liveblocks data to Neon. In this example, we sync Liveblocks Storage data to Neon, but you can use the same pattern with other APIs and webhooks to sync other types of data. Create the Neon table Use one row for each Liveblocks room. ```sql create table liveblocks_documents ( room_id text primary key, data jsonb not null, updated_at timestamptz not null default now() ); ``` Create a webhook endpoint Add a back end endpoint in your app, for example at `/api/liveblocks-webhook`. ```ts export async function POST(request: Request) { const body = await request.json(); const headers = request.headers; // Verify the webhook event, then sync to Neon // ... return new Response(null, { status: 200 }); } ``` Subscribe to Storage updates In the [Liveblocks dashboard](/dashboard), navigate to the “Webhooks’ page inside a project, and create a webhook endpoint for your endpoint URL—this requires you to [host your local project](/docs/guides/how-to-test-webhooks-on-localhost). Subscribe to [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent), then copy the webhook secret. Verify the webhook event In your endpoint, using [`WebhookHandler`](/docs/api-reference/liveblocks-node#WebhookHandler), verify the webhook event with the webhook secret from the dashboard. ```ts import { WebhookHandler } from "@liveblocks/node"; const webhookHandler = new WebhookHandler( process.env.LIVEBLOCKS_WEBHOOK_SECRET! ); 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 }); } // +++ // Sync to Neon // ... return new Response(null, { status: 200 }); } ``` Sync Storage to Neon Set up your Liveblocks and Neon clients, before fetching the Storage document data with [`getStorageDocument`](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-storage) and upserting the Neon row with `on conflict … do update`. ```ts import { Liveblocks, WebhookHandler } from "@liveblocks/node"; import { neon } from "@neondatabase/serverless"; const liveblocks = new Liveblocks({ secret: process.env.LIVEBLOCKS_SECRET_KEY!, }); const sql = neon(process.env.DATABASE_URL!); const webhookHandler = new WebhookHandler( process.env.LIVEBLOCKS_WEBHOOK_SECRET! ); 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 (event.type === "storageUpdated") { const { roomId } = event.data; // Get Storage document data const data = await liveblocks.getStorageDocument(roomId, "json"); // Upsert into Neon await sql` insert into liveblocks_documents (room_id, data, updated_at) values (${roomId}, ${JSON.stringify(data)}::jsonb, now()) on conflict (room_id) do update set data = excluded.data, updated_at = excluded.updated_at `; } // +++ return new Response(null, { status: 200 }); } ``` Data sync is set up! Your Liveblocks data is now automatically synched to Neon when the webhook event is fired. ## Limits and troubleshooting ### Storage or Yjs data is stale [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent) and [`ydocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent) webhooks are throttled because collaborative documents can be modified up to 60 times per second. Treat Neon as an eventually consistent mirror, not as the live editing channel. ### Webhook verification fails Check that `LIVEBLOCKS_WEBHOOK_SECRET` is the webhook secret for the webhook endpoint that sent the event. Also make sure your endpoint passes the same raw body string to [`verifyRequest`](/docs/api-reference/liveblocks-node#verifyRequest) that it received from Liveblocks. ### Neon writes fail Keep `DATABASE_URL` on the server only. Use a role with permission to write the mirror tables. If you use Neon’s pooled connection string, follow Neon’s guidance for serverless and long-running workers. ### Duplicate writes happen Webhook deliveries can be retried. Use `on conflict … do update` upserts with a stable primary key such as `room_id`, `thread_id`, or `comment_id` so repeated deliveries update the same row. ### Liveblocks REST requests fail Check that `LIVEBLOCKS_SECRET_KEY` is a secret key from the same Liveblocks project as the room. If the request still fails, return a non-2xx response so the webhook can be retried. ## Related docs - [Supabase + Liveblocks](/docs/integrations/supabase). - API references for, [webhooks](/docs/platform/webhooks), [Node.js](/docs/api-reference/liveblocks-node), [Python](/docs/api-reference/liveblocks-python), and [REST API](/docs/api-reference/rest-api-endpoints). --- meta: title: "PlanetScale + Liveblocks" parentTitle: "Integrations" description: "Use PlanetScale with Liveblocks when your collaborative app needs to store data in a MySQL database—for example mirrored collaboration data for reporting, search, audit logs, and workflows." --- [PlanetScale](https://planetscale.com/) provides a serverless MySQL database with horizontal scaling and branching. Using webhooks, you can set up one-way synchronization of your Liveblocks data to PlanetScale for reporting, search, audit logs, or app workflows. ## How data sync works Liveblocks [webhooks](/docs/platform/webhooks) trigger when certain events happen, such as when a collaborative document updates. Liveblocks can trigger an endpoint in your back end, and from here, you can fetch the latest data and write it to PlanetScale. Here’s an example of how it works with [Liveblocks Storage](/docs/collaboration-features/multiplayer/sync-engine/liveblocks-storage). ```mermaid sequenceDiagram actor User participant LB as Liveblocks app participant Webhook as Webhook endpoint participant PlanetScale User->>LB: Edits document Note over LB: Throttle LB->>Webhook: **storageUpdated** webhook Webhook->>LB: **getStorageDocument** API LB-->>Webhook: Storage data Webhook->>PlanetScale: Upsert row data PlanetScale-->>Webhook: 200 OK ``` ## Which data can be synced? Various types of Liveblocks data can be synched to PlanetScale with webhooks. | Name | Description | Relevant webhook | Relevant API | | ------------------------ | ------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | Rooms | Created rooms and metadata. | [`roomUpdated`](/docs/platform/webhooks#RoomUpdatedEvent) | [`getRoom`](/docs/api-reference/liveblocks-node#get-rooms-roomId) | | Active users | Currently connected users. | [`userEntered`](/docs/platform/webhooks#UserEnteredEvent) | [`getActiveUsers`](/docs/api-reference/liveblocks-node#get-active-users) | | Liveblocks Storage | Custom realtime document state. | [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent) | [`getStorageDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-storage) | | React Flow | Flowchart state. | [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent) | [`mutateFlow`](/docs/api-reference/liveblocks-react-flow#mutateFlow) | | Yjs | `Y.Doc` document state. | [`ydocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent) | [`getYjsDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc) | | Tiptap/BlockNote/Lexical | Text editor state. | [`ydocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent) | [`getYjsDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc) | | Threads | Comments, reactions, more. | [`threadCreated`](/docs/platform/webhooks#ThreadCreatedEvent) | [`getThread`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId) | This is not an exhaustive list—around [15 related webhook events](/docs/platform/webhooks#Liveblocks-events) are available, along with many [Node.js methods](/docs/api-reference/liveblocks-node), [Python functions](/docs/api-reference/liveblocks-python), and [REST API endpoints](/docs/api-reference/rest-api-endpoints). ## Setup Quickstart for synching Liveblocks data to PlanetScale. In this example, we sync Liveblocks Storage data to PlanetScale, but you can use the same pattern with other APIs and webhooks to sync other types of data. We have a [full step-by-step guide available here](/docs/guides/how-to-synchronize-your-liveblocks-storage-document-data-to-a-planetscale-mysql-database), this page provides a quick summary. Create the PlanetScale table Use one row for each Liveblocks room. ```sql create table liveblocks_documents ( room_id varchar(255) primary key, data json not null, updated_at timestamp not null default current_timestamp on update current_timestamp ); ``` Create a webhook endpoint Add a back end endpoint in your app, for example at `/api/liveblocks-webhook`. ```ts export async function POST(request: Request) { const body = await request.json(); const headers = request.headers; // Verify the webhook event, then sync to PlanetScale // ... return new Response(null, { status: 200 }); } ``` Subscribe to Storage updates In the [Liveblocks dashboard](/dashboard), navigate to the “Webhooks’ page inside a project, and create a webhook endpoint for your endpoint URL—this requires you to [host your local project](/docs/guides/how-to-test-webhooks-on-localhost). Subscribe to [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent), then copy the webhook secret. Verify the webhook event In your endpoint, using [`WebhookHandler`](/docs/api-reference/liveblocks-node#WebhookHandler), verify the webhook event with the webhook secret from the dashboard. ```ts import { WebhookHandler } from "@liveblocks/node"; const webhookHandler = new WebhookHandler( process.env.LIVEBLOCKS_WEBHOOK_SECRET! ); 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 }); } // +++ // Sync to PlanetScale // ... return new Response(null, { status: 200 }); } ``` Sync Storage to PlanetScale Set up your Liveblocks and PlanetScale clients, before fetching the Storage document data with [`getStorageDocument`](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-storage) and upserting the PlanetScale row with `on duplicate key update`. ```ts import { Liveblocks, WebhookHandler } from "@liveblocks/node"; import { connect } from "@planetscale/database"; const liveblocks = new Liveblocks({ secret: process.env.LIVEBLOCKS_SECRET_KEY!, }); const db = connect({ url: process.env.DATABASE_URL! }); const webhookHandler = new WebhookHandler( process.env.LIVEBLOCKS_WEBHOOK_SECRET! ); 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 (event.type === "storageUpdated") { const { roomId } = event.data; // Get Storage document data const data = await liveblocks.getStorageDocument(roomId, "json"); // Upsert into PlanetScale await db.execute( `insert into liveblocks_documents (room_id, data) values (?, ?) on duplicate key update data = values(data)`, [roomId, JSON.stringify(data)] ); } // +++ return new Response(null, { status: 200 }); } ``` Data sync is set up! Your Liveblocks data is now automatically synched to PlanetScale when the webhook event is fired. ## Limits and troubleshooting ### Storage or Yjs data is stale [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent) and [`ydocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent) webhooks are throttled because collaborative documents can be modified up to 60 times per second. Treat PlanetScale as an eventually consistent mirror, not as the live editing channel. ### Webhook verification fails Check that `LIVEBLOCKS_WEBHOOK_SECRET` is the webhook secret for the webhook endpoint that sent the event. Also make sure your endpoint passes the same raw body string to [`verifyRequest`](/docs/api-reference/liveblocks-node#verifyRequest) that it received from Liveblocks. ### PlanetScale writes fail Keep `DATABASE_URL` on the server only. Use a database password with permission to write the mirror tables. PlanetScale doesn’t support foreign keys by default—if you need referential integrity, model it in application code. ### Duplicate writes happen Webhook deliveries can be retried. Use `on duplicate key update` upserts with a stable primary key such as `room_id`, `thread_id`, or `comment_id` so repeated deliveries update the same row. ### Liveblocks REST requests fail Check that `LIVEBLOCKS_SECRET_KEY` is a secret key from the same Liveblocks project as the room. If the request still fails, return a non-2xx response so the webhook can be retried. ## Related docs - [Synchronize Liveblocks Storage document data to PlanetScale MySQL](/docs/guides/how-to-synchronize-your-liveblocks-storage-document-data-to-a-planetscale-mysql-database). - [Synchronize Liveblocks Yjs document data to PlanetScale MySQL](/docs/guides/how-to-synchronize-your-liveblocks-yjs-document-data-to-a-planetscale-mysql-database). - API references for, [webhooks](/docs/platform/webhooks), [Node.js](/docs/api-reference/liveblocks-node), [Python](/docs/api-reference/liveblocks-python), and [REST API](/docs/api-reference/rest-api-endpoints). --- meta: title: "Replit + Liveblocks" parentTitle: "Integrations" description: "Add Liveblocks to apps built with Replit Agent so you can ship realtime presence, multiplayer editors, and comments alongside generated UI." --- [Replit](https://replit.com/) lets you build and run full-stack apps in the browser using Replit Agent. With Liveblocks, you can add realtime collaboration to your generated Replit app, such as [collaborative text editing](/docs/collaboration-features/multiplayer/text-editor), [multiplayer documents](/docs/collaboration-features/multiplayer/sync-engine), [comment threads](/docs/collaboration-features/comments), [inbox notifications](/docs/collaboration-features/notifications), and [AI copilots](/docs/collaboration-features/ai-copilots). ## Setup Ask the Agent to build with Liveblocks Add Liveblocks to your project by asking Replit Agent to add collaborative features, for example: ```text title="Example prompt" Add a realtime text editor with Liveblocks and Tiptap. ``` To find more ways to use Liveblocks, visit our [showcase](/showcase) and read our [get started guides](/docs/get-started). Insert your secret key During the process, Replit Agent will prompt you to insert your Liveblocks secret key from the [dashboard](/dashboard/apikeys). Paste in your project’s key. ```text title="Secret key" {{SECRET_KEY}} ``` To manually edit your key in Replit, go to Tools & Files → Secrets → `LIVEBLOCKS_SECRET_KEY`. Collaboration is ready Your app should now have live collaboration—use the Agent to continue building your Liveblocks app. ## Limitations and troubleshooting ### Secret or public key issues To fix issues related to API keys, instruct Replit Agent to use a secret key instead of a public key, and authenticate your Liveblocks application. --- meta: title: "Supabase + Liveblocks" parentTitle: "Integrations" description: "Use Supabase with Liveblocks when your collaborative app needs to store data in a Postgres database—for example mirrored collaboration data for reporting, search, audit logs, and workflows." --- [Supabase](https://supabase.com/) provides a Postgres database with a rich set of features. Using webhooks, you can set up one-way synchronization of your Liveblocks data to Supabase for reporting, search, audit logs, or app workflows. ## How data sync works Liveblocks [webhooks](/docs/platform/webhooks) trigger when certain events happen, such as when a collaborative document updates. Liveblocks can trigger an endpoint in your back end, and from here, you can fetch the latest data and write it to Supabase. Here’s an example of how it works with [Liveblocks Storage](/docs/collaboration-features/multiplayer/sync-engine/liveblocks-storage). ```mermaid sequenceDiagram actor User participant LB as Liveblocks app participant Webhook as Webhook endpoint participant Supabase User->>LB: Edits document Note over LB: Throttle LB->>Webhook: **storageUpdated** webhook Webhook->>LB: **getStorageDocument** API LB-->>Webhook: Storage data Webhook->>Supabase: Upsert row data Supabase-->>Webhook: 200 OK ``` ## Which data can be synced? Various types of Liveblocks data can be synched to Supabase with webhooks. | Name | Description | Relevant webhook | Relevant API | | ------------------------ | ------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | Rooms | Created rooms and metadata. | [`roomUpdated`](/docs/platform/webhooks#RoomUpdatedEvent) | [`getRoom`](/docs/api-reference/liveblocks-node#get-rooms-roomId) | | Active users | Currently connected users. | [`userEntered`](/docs/platform/webhooks#UserEnteredEvent) | [`getActiveUsers`](/docs/api-reference/liveblocks-node#get-active-users) | | Liveblocks Storage | Custom realtime document state. | [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent) | [`getStorageDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-storage) | | React Flow | Flowchart state. | [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent) | [`mutateFlow`](/docs/api-reference/liveblocks-react-flow#mutateFlow) | | Yjs | `Y.Doc` document state. | [`ydocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent) | [`getYjsDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc) | | Tiptap/BlockNote/Lexical | Text editor state. | [`ydocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent) | [`getYjsDocument`](/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc) | | Threads | Comments, reactions, more. | [`threadCreated`](/docs/platform/webhooks#ThreadCreatedEvent) | [`getThread`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId) | This is not an exhaustive list—around [15 related webhook events](/docs/platform/webhooks#Liveblocks-events) are available, along with many [Node.js methods](/docs/api-reference/liveblocks-node), [Python functions](/docs/api-reference/liveblocks-python), and [REST API endpoints](/docs/api-reference/rest-api-endpoints). ## Setup Quickstart for synching Liveblocks data to Supabase. In this example, we sync Liveblocks Storage data to Supabase, but you can use the same pattern with other APIs and webhooks to sync other types of data. We have a [full step-by-step guide available here](/docs/guides/how-to-synchronize-your-liveblocks-storage-document-data-to-a-supabase-postgres-database), this page provides a quick summary. Create the Supabase table Use one row for each Liveblocks room. ```sql create table liveblocks_documents ( room_id text primary key, data jsonb not null, updated_at timestamptz not null default now() ); ``` Create a webhook endpoint Add a back end endpoint in your app, for example at `/api/liveblocks-webhook`. ```ts export async function POST(request: Request) { const body = await request.json(); const headers = request.headers; // Verify the webhook event, then sync to Supabase // ... return new Response(null, { status: 200 }); } ``` Subscribe to Storage updates In the [Liveblocks dashboard](/dashboard), navigate to the “Webhooks’ page inside a project, and create a webhook endpoint for your endpoint URL—this requires you to [host your local project](/docs/guides/how-to-test-webhooks-on-localhost). Subscribe to [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent), then copy the webhook secret. Verify the webhook event In your endpoint, using [`WebhookHandler`](/docs/api-reference/liveblocks-node#WebhookHandler), verify the webhook event with the webhook secret from the dashboard. ```ts import { WebhookHandler } from "@liveblocks/node"; const webhookHandler = new WebhookHandler( process.env.LIVEBLOCKS_WEBHOOK_SECRET! ); 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 }); } // +++ // Sync to Supabase // ... return new Response(null, { status: 200 }); } ``` Sync Storage to Supabase Set up your Liveblocks and Supabase clients, before fetching the Storage document data with [`getStorageDocument`](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-storage) and upserting the Supabase row with `upsert`. ```ts import { Liveblocks, WebhookHandler } from "@liveblocks/node"; import { createClient } from "@supabase/supabase-js"; const liveblocks = new Liveblocks({ secret: process.env.LIVEBLOCKS_SECRET_KEY!, }); const supabase = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! ); const webhookHandler = new WebhookHandler( process.env.LIVEBLOCKS_WEBHOOK_SECRET! ); 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 (event.type === "storageUpdated") { const { roomId } = event.data; // Get Storage document data const data = await liveblocks.getStorageDocument(roomId, "json"); // Upsert into Supabase const { error } = await supabase.from("liveblocks_documents").upsert({ room_id: roomId, data, updated_at: new Date().toISOString(), }); } // +++ return new Response(null, { status: 200 }); } ``` Data sync is set up! Your Liveblocks data is now automatically synched to Supabase when the webhook event is fired. ## Limits and troubleshooting ### Storage or Yjs data is stale [`storageUpdated`](/docs/platform/webhooks#StorageUpdatedEvent) and [`ydocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent) webhooks are throttled because collaborative documents can be modified up to 60 times per second. Treat Supabase as an eventually consistent mirror, not as the live editing channel. ### Webhook verification fails Check that `LIVEBLOCKS_WEBHOOK_SECRET` is the webhook secret for the webhook endpoint that sent the event. Also make sure your endpoint passes the same raw body string to [`verifyRequest`](/docs/api-reference/liveblocks-node#verifyRequest) that it received from Liveblocks. ### Supabase writes fail Keep the Supabase service role key on the server. If Row Level Security is enabled on the mirror table, make sure your server client has permission to write to it. ### Duplicate writes happen Webhook deliveries can be retried. Use `upsert` with a stable primary key such as `room_id`, `thread_id`, or `comment_id` so repeated deliveries update the same row. ### Liveblocks REST requests fail Check that `LIVEBLOCKS_SECRET_KEY` is a secret key from the same Liveblocks project as the room. If the request still fails, return a non-2xx response so the webhook can be retried. ## Related docs - [Synchronize Liveblocks Storage document data to Supabase Postgres](/docs/guides/how-to-synchronize-your-liveblocks-storage-document-data-to-a-supabase-postgres-database). - [Synchronize Liveblocks Yjs document data to Supabase Postgres](/docs/guides/how-to-synchronize-your-liveblocks-yjs-document-data-to-a-supabase-postgres-database). - API references for, [webhooks](/docs/platform/webhooks), [Node.js](/docs/api-reference/liveblocks-node), [Python](/docs/api-reference/liveblocks-python), and [REST API](/docs/api-reference/rest-api-endpoints). # Liveblocks + Supabase — context for your assistant **Purpose:** Help users mirror Liveblocks Storage, Yjs, Comments, and Threads into Supabase Postgres via webhooks and the REST API. **Do:** - Treat Liveblocks as the source of truth for Storage, Yjs, Comments, and Threads; treat Supabase as an eventually consistent mirror. - Trigger syncs from `storageUpdated`, `ydocUpdated`, and thread/comment webhooks; fetch current state with the Liveblocks REST API; `upsert` into Supabase. - Verify webhooks with `WebhookHandler` from `@liveblocks/node`, passing the exact raw body received. - Use stable primary keys (`room_id`, `thread_id`, `comment_id`) so retried deliveries update the same row. - Keep the Supabase service role key server-only and confirm RLS allows the server client to write to the mirror table. **Do not:** - Conflate `LIVEBLOCKS_SECRET_KEY` (REST) with `LIVEBLOCKS_WEBHOOK_SECRET` (signature verification). - Use `insert` for webhook-driven writes, or omit a stable primary key — duplicates will appear on retry. - Re-stringify a parsed JSON body before `verifyRequest`; pass the raw body Liveblocks sent. - Treat Supabase as the live editing channel — `storageUpdated` and `ydocUpdated` are throttled. - Mirror more Comments or Threads fields than the user's workflow needs. Tailor examples to the user's stack (Next.js route handler, Express, etc.) and the specific Liveblocks feature they're syncing. --- meta: title: "v0 + Liveblocks" parentTitle: "Integrations" description: "Add Liveblocks to apps built or exported from v0 so you can ship realtime presence, multiplayer editors, and comments alongside generated UI." --- [v0](https://v0.app/) helps you generate full-stack apps quickly using AI chats. With Liveblocks, you can add realtime collaboration to your generated v0 app, such as [collaborative text editing](/docs/collaboration-features/multiplayer/text-editor), [multiplayer documents](/docs/collaboration-features/multiplayer/sync-engine), [comment threads](/docs/collaboration-features/comments), [inbox notifications](/docs/collaboration-features/notifications), and [AI copilots](/docs/collaboration-features/ai-copilots). ## Setup Ask the chat to build with Liveblocks Add Liveblocks to your project by asking the chat to add collaborative features, for example: ```text title="Example prompt" Add a realtime text editor with Liveblocks and Tiptap. ``` To find more ways to use Liveblocks, visit our [showcase](/showcase) and read our [get started guides](/docs/get-started). Insert your secret key During the process, v0 will prompt you to insert your Liveblocks secret key from the [dashboard](/dashboard/apikeys). Paste in your project’s key. ```text title="Secret key" {{SECRET_KEY}} ``` To manually edit your key in v0, go to Settings → Environment Variables → `LIVEBLOCKS_SECRET_KEY`. Collaboration is ready Your app should now have live collaboration—use the chat to continue building your Liveblocks app. ## Limitations and troubleshooting ### Secret or public key issues To fix issues related to API keys, instruct v0 to use a secret key instead of a public key, and authenticate your Liveblocks application. --- meta: title: "Integrations" description: "Connect Liveblocks to databases, workflow automation, AI tools, frameworks, and other products your team uses." --- Liveblocks integrates with products and services your team already uses, including databases, workflow automation, deployment platforms, AI-native editors, and other tools and frameworks you combine with realtime collaboration. ## AI tools Develop Liveblocks apps more easily with our agent skills and MCP server for your favorite tools. } description="Add our agent skills and MCP server to Cursor." /> } description="Add our agent skills and MCP server to OpenAI Codex." /> } description="Add our agent skills and MCP server to Claude Code." /> } description="Add our agent skills and MCP server to Claude Desktop." /> ## Databases Liveblocks doesn’t require a database, but you can mirror your data for searching, logging, and more. } title="Supabase" href="/docs/integrations/supabase" description="Mirror Storage, Yjs, Comments, and Threads into Supabase Postgres." /> } title="Neon" href="/docs/integrations/neon" description="Mirror Storage, Yjs, Comments, and Threads into Neon Postgres." /> } title="PlanetScale" href="/docs/integrations/planetscale" description="Mirror Storage, Yjs, Comments, and Threads into PlanetScale MySQL." /> ## App builders Add Liveblocks realtime collaboration to your AI app builder projects. } description="Realtime collaboration for apps you build or export from Lovable." /> } description="Realtime collaboration for apps you build or export from v0." /> } description="Realtime collaboration for apps you build or export from Bolt." /> } description="Realtime collaboration for apps you build with Replit Agent." /> ## Workflow automation Use Liveblocks REST API endpoints in UI workflow tools. } description="REST API calls and webhooks via Liveblocks nodes in n8n workflows." /> ## Build your own integration Use [webhooks](/docs/platform/webhooks) and the [REST API](/docs/api-reference/rest-api-endpoints) to connect Liveblocks to other databases, automation tools, app builders, or internal systems. If you’d like to see an integration guide that is not currently available, [let us know on Discord](https://liveblocks.io/discord). --- 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." --- _Multi-factor authentication_ (MFA) adds an additional layer of security by requiring a code from an authenticator app when signing in. Liveblocks offers two ways to use MFA: - **User-level enrollment**: Individual users voluntarily enable MFA for their own account. - **Enterprise enforcement**: Team owners require all team members to have MFA enabled. Only for enterprise customers, does not apply to Single Sign-On members. ## Enroll MFA for your account Any user can enable MFA on their personal account for extra security. This is optional and applies only to your own sign-in. ### How to enroll MFA for your account 1. Go to [**Personal settings**](https://liveblocks.io/dashboard/personal-settings) in the dashboard. 2. In the **Two-factor authentication** section, click **Enable**.
Two-factor authentication section in account settings with Enable button
3. In the setup modal, scan the QR code with your authenticator app, or copy the setup key and paste it into your app. 4. Enter the 6-digit code from your authenticator app and click **Confirm**.
Set up two-factor authentication modal with QR code and code verification
### How to disable MFA for your account 1. Go to [**Personal settings**](https://liveblocks.io/dashboard/personal-settings) in the dashboard. 2. In the **Two-factor authentication** section, click **Disable**. 3. Enter your password and click **Confirm**.
Two-factor authentication section in account settings with Disable button
An alert will be prompted to confirm the action. In our current set up if you are a member of an enterprise team enforcing MFA for all its members, next time you sign in you will be prompted to enroll MFA again.
Alert to confirm the action of disabling MFA
## Enforce MFA for all team members 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. When MFA is enforced at the team level, all members must have an authenticator app. They are prompted to enter a code when signing in to the dashboard.
Liveblocks team setup MFA
### How to enable MFA enforcement MFA enforcement 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 works 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: "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: "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: "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 Using multiple free teams or accounts to reduce usage 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 and Team | | --------------------------------- | -------------------------------------------- | ------------------------------------------- | | 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 and Team plans, you can pay for [additional usage](/docs/platform/limits) as you go. ### Additional usage For members of our Pro and Team plans, we offer a pay-as-you-go model for additional usage, giving you greater flexibility and control over your usage. The typical monthly usage guidelines above are still applicable, while extra usage will be automatically charged at the following rates: | Item | Included | Pay-as-you-go | | -------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------ | | 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, Team or Enterprise subscription. We consider usage to be commercial when a project generates revenue or financial benefit for anyone involved in its development, operation, or maintenance. This includes projects built by paid employees, contractors, or consultants. Internal use within a company qualifies as commercial usage, regardless of whether users pay directly. Common examples of commercial usage include: - Applications that accept payments or process transactions - Products or services offered for sale - Projects developed or maintained for compensation - Sites primarily focused on affiliate marketing - Applications displaying advertisements or sponsored content All environments and projects associated with a commercial product must be on a paid plan, including development, staging, and testing environments. 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: "Limits" parentTitle: "Platform" description: "A list of all the limits and limitations that apply on Liveblocks." --- ## General limits There are three plans: Free, Pro, Team, and Enterprise. Each plan has its own set of limits. | | Free | Pro | Team | Enterprise | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Monthly active rooms | | included
| included
| Up to | | Monthly active users | | | | | | Projects | | | | Up to | | Team members | | included
| 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
| 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
| 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
| 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 | 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. | 32 MB Limited to 1 MB for client versions below 3.14. Make sure to upgrade to get higher limits. | | Thread metadata Custom metadata attached to a thread. | 50 properties, key length 40 characters, value length 4000 characters | | Comment metadata Custom metadata attached to a comment. | 50 properties, key length 40 characters, value length 4000 characters | | `LiveObject` A realtime data structure that stores key-value pairs. Learn more | 2 MB when totalling the size of the keys and values For rooms powered by the v2 Storage engine. Rooms powered by the classic v1 engine are limited to 128 kB. | | `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 2 MB For rooms powered by the v2 Storage engine. Rooms powered by the classic v1 engine are limited to 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 2 MB For rooms powered by the v2 Storage engine. Rooms powered by the classic v1 engine are limited to 128 kB. | Note that when one realtime data structure is nested inside another, it does not count towards the limit. Only the JSON leaves of your data structure count towards the limit. For example, if a `LiveList` is nested inside a `LiveObject`, the `LiveList` and its contents do not count towards the `LiveObject`'s data limit. ## FAQs ### What is a monthly active room? Monthly active rooms represents the number of rooms that have been used within a given month. Rooms are identified by their `roomId`. Specifically, a room counts as active when any of these occur: - A user connects on the front-end. - A comment is added or modified. - A document is updated. Any of the following: Storage, Tiptap, BlockNote, Lexical, Yjs. - An attachment is uploaded. If a room contains content from the previous month, it is not active until it is used in the current month. For example, if a text document is created in January, it is not active in February until a user connects to the document or updates its content. ### What is a monthly active user? Monthly active users represents the number of users that have used any Liveblocks features within a given month. Users are identified by the `userId` property provided in your [authentication endpoint](/docs/authentication). This `userId` generally comes from your database or your authentication provider. One unique `userId` corresponds to one user, no matter how long they connect for. For example, a user that connects for 5 minutes on 2 different days is counted as 1 MAU, and a user that connects 8 hours a day for a month is also counted as 1 MAU. This applies so long as usage falls under our [Fair Use Policy](/docs/platform/limits/fair-use-policy). ### What’s a connection? A connection occurs when a user opens a web page containing a Liveblocks room. Each tab counts as one connection, which means that a user with two open tabs has two connections. A room is joined when using{" "} [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider) in React or [`enterRoom`](/docs/api-reference/liveblocks-client#Client.enterRoom) in JavaScript. Liveblocks connections are analogous to WebSocket connections, representing a realtime link to our server. ### What’s a room? A room is the virtual space where people collaborate. For most products, a room corresponds to a document. ### What’s a simultaneous connection per room? A simultaneous connection per room is when a user or multiple users are connected to the same Liveblocks room at the same time. Learn more about handling simultaneous room connection limits in our guide about [joining rooms at maximum capacity](/docs/guides/what-happens-when-a-user-joins-a-room-at-maximum-capacity). ### What’s a simultaneous connection per project? A simultaneous connection per project is when a user or multiple users are connected to the same project at the same time. ### How is realtime data storage calculated? Realtime data storage is a cumulative calculation that represents all realtime data that is currently stored in your rooms. It does not reset monthly, and the total usage will continue to accrue over time as you add more content and data. You can monitor your usage at any time in the [dashboard](/dashboard). ### What happens when limits are reached? Liveblocks returns a different error for each limit that a user might reach. That way, you can decide how to best handle those cases in your product. ### Do you count monthly active rooms during testing? Yes, if test users connect to a room during automated testing, the room will be counted as a monthly active room. To prevet hitting limits during testing, you may want to provide a set of reusable `roomId`s. --- meta: title: "Management API" parentTitle: "Platform" description: "Learn how to use the Liveblocks Management API." --- The Management API is currently in private beta for Enterprise customers. Reach out to [sales@liveblocks.io](mailto:sales@liveblocks.io) to enable it on your team. The Liveblocks Management API allows you to interact programmatically with your Liveblocks projects and webhooks. With the API, you can create, update, and delete projects, webhooks, and more. The Liveblocks Management 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](/docs/api-reference/rest-api-endpoints#Management) for more information. ``` https://api.liveblocks.io/v2/management ``` ## How it works When Management API is enabled, you'll need to authenticate your requests using an access token. This access token is generated from your Liveblocks team and can be found in the [dashboard](/dashboard/team-settings) teams settings page under the **Management API** section.
Liveblocks management API access tokens
Management API access tokens are team-linked and can only be created by team owners. If you don't have team owner permissions, contact your team owner to generate a token for you. ## Generating an access token To generate a new Management API access token click the “**Generate new token…**” button to open the token generation dialog.
Generate new access token dialog
In the dialog, some options are available. ### Token name A name for your token, which is used to help you identify and manage your tokens. ```text title="Token name" LB-MANAGEMENT-API-ACCESS-TOKEN ``` ### Scopes Scopes that define your token's permissions. You can select one or both scopes depending on your needs. - **Read**: Allows viewing data but not modifying it. Grants read access to all resources. - **Write**: Allows creating, modifying, and deleting data. Grants write access to all resources. ```text title="Scopes" ✔︎ Read ✔︎ Write ``` More granular scopes will be available in a future update. If you'd like to provide feedback or have specific scope requirements, please contact [sales@liveblocks.io](mailto:sales@liveblocks.io). ### Generate new token Once you've configured your token name and scopes, click **Generate new token** to create it. After generation, a dialog will appear displaying your new Management API token. This is the **only time** you'll be able to view and copy the full token value.
New access token dialog
**Save and store this token in a secure place**, such as a password manager or secret store. You won't be able to see it again after closing this dialog. Use the copy icon in the dialog to copy the token to your clipboard before clicking “**Done**”. After you close the dialog, your new token will appear in the Management API tokens list. The token value will be partially obscured for security, showing only the first few characters. You can delete any token at any time using the delete icon next to it. ## Access tokens expiry All Management API access tokens have an expiration date. When generating a new token, you can choose from the following expiration options: - **7 days**: Token expires 7 days from creation - **30 days**: Token expires 30 days from creation - **90 days**: Token expires 90 days from creation - **1 year**: Token expires 1 year from creation (default) - **Custom date**: Choose a specific expiration date, up to a maximum of 1 year from today The expiration date is displayed when you generate a new token, and you can view it for existing tokens in the Management API tokens list. When a token expires, you'll need to generate a new token to continue using the Management API. Make sure to rotate your tokens before they expire to avoid interruptions in your API access. ### Expiration reminders Liveblocks sends email reminders to team owners before tokens expire. You'll receive notifications at 7 days and 1 day before expiration, giving you time to generate replacement tokens and update your integrations. ## Revoking an access token If you need to revoke a Management API access token, click the delete icon next to the token in the Management API tokens list. A confirmation dialog will appear asking you to confirm the revocation.
Revoke access token confirmation dialog
Revoking a token will immediately block Management API access for any application or script using it. This may break integrations that depend on the token. Once revoked, you cannot restore the token and will need to generate a new one if you need to restore access. Before revoking a token, make sure to: - Verify that no active integrations or scripts are using the token - Generate a replacement token if needed, and update your integrations to use the new token - Confirm that revoking the token won't disrupt critical operations If you're certain you want to proceed, click “**Yes I understand, revoke token**” in the confirmation dialog. The token will be immediately revoked and removed from your tokens list. ## Usage The Management API can be called from any terminals or in CI/CI pipelines using the `curl` command. Pass your management token, e.g. `lbs_sd7H24...`, in the `Authorization` header. ```bash highlight="2-3" # >_ terminal curl -X GET "https://api.liveblocks.io/v2/management/projects" \ -H "Authorization: Bearer lbs_*******************" \ -H "Content-Type: application/json" ``` ```yaml highlight="9-10" # >_ CI/CD workflow name: Liveblocks Management API jobs: projects: runs-on: ubuntu-latest steps: - name: List all projects with the Management API run: | curl -X GET "https://api.liveblocks.io/v2/management/projects" \ -H "Authorization: Bearer lbs_*******************" \ -H "Content-Type: application/json" ``` ## Rate limiting The Management API enforces rate limits to ensure fair usage and system stability. By default, each Management API access token (MAAT) is limited to **1,000 requests per 60 seconds**. This limit applies cumulatively across all requests made with a single access token. If you exceed this limit, all API calls using that token will be blocked for the next minute, and the API will return a `429 Too Many Requests` HTTP status response. Rate limits are subject to our fair use policy. If you need higher rate limits for your use case, please contact [sales@liveblocks.io](mailto:sales@liveblocks.io) to discuss your requirements. ## API reference See the [API reference](/docs/api-reference/rest-api-endpoints#Management) for more information. --- 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](/docs/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 the following IP addresses. ### Global and US ``` 44.228.126.217 50.112.21.217 52.24.126.164 54.148.139.208 2600:1f24:64:8000::/56 ``` ### EU ``` 52.215.16.239 54.216.8.72 63.33.109.123 2a05:d028:17:8000::/56 ``` ## 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/CommentMetadataUpdated` - `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", }, }; ``` #### CommentMetadataUpdatedEvent An event is triggered when a comment’s metadata is updated. This event is not throttled. ```ts // Schema type CommentMetadataUpdatedEvent = { type: "commentMetadataUpdated"; data: { projectId: string; roomId: string; threadId: string; commentId: string; updatedAt: string; updatedBy: string; }; }; // Example const commentMetadataUpdatedEvent = { type: "commentMetadataUpdated", data: { projectId: "my-project-id", roomId: "my-room-id", threadId: "my-thread-id", commentId: "my-comment-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: "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 Team, 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. ## Included usage | Item | Free included usage | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ | | Realtime collaboration minutes The total active time people and agents spent together inside rooms. | included | | Comments created The number of comments created in rooms. | included | | Realtime data storage updates Number of Liveblocks Storage row updates in the selected period, aggregated across the projects you include in the filter. | included | | Realtime data stored 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 | | 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 | | 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 | ## Other limits | Item | Free limit | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------- | | Monthly active users | | | Monthly active rooms | included | | Projects | included | | Dashboard seats Team members with Owner or Member role. They can configure projects, roll keys, and more. | included | | Simultaneous connections per room | | | Simultaneous connections per project | | | Monthly anonymous connections Public key only recommended for prototyping. Set up authentication. | | | Max file upload size | | | 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 | | | Environments | | ## 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) or [Team](/dashboard/billing?plan=team) to access monthly credits and ongoing metered usage. --- meta: title: "Liveblocks Pro plan" parentTitle: "Plans" description: "Learn about the Liveblocks Pro plan." --- The Pro plan is designed for developers shipping collaborative experiences in production. It includes everything in Free, but enables you to remove the "Powered by Liveblocks" badge, and includes unlimited monthly active rooms. You can also buy the Unlimited Version History add-on. ## Pricing | Monthly credits | Monthly price | Annual price | | ---------------------------- | ------------------------------------ | ----------------------------------------------------------- | | | per month | per month billed annually | Annual Pro billing gives you 2 months free. ## Metered usage | Item | Pro pricing | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | | Realtime collaboration minutes The total active time people and agents spent together inside rooms. | | | Comments created The number of comments created in rooms. | | | Realtime data storage updates Number of Liveblocks Storage row updates in the selected period, aggregated across the projects you include in the filter. | | | Realtime data stored 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. | | | 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. | | | 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. | | ## Other limits | Item | Pro limit | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | | Monthly active users | | | Monthly active rooms | | | Projects | included | | Dashboard seats Team members with Owner or Member role. They can configure projects, roll keys, and more. | included | | Simultaneous connections per room | | | Simultaneous connections per project | | | Monthly anonymous connections Public key only recommended for prototyping. Set up authentication. | | | Average connections per user | | | Active room hours per month | | | Max file upload size | | | 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 add-on | | Event log retention | | | Environments | | ## 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. Monthly credits renew at the beginning of each billing cycle and are applied to metered usage. Any usage not covered by credits appears on your invoice. ## Add-ons | Add-on | Price | | ------------------------- | ---------------------------------------------------------------------------------------------------- | | Unlimited Version History | per month Billed annually | --- meta: title: "Liveblocks Team plan" parentTitle: "Plans" description: "Learn about the Liveblocks Team plan." --- The Team plan is designed for teams shipping collaborative experiences in production with the ability to scale. It includes everything in Pro, plus SAML Single Sign-on (SSO), SOC 2, and a private Slack channel. You can also request a HIPAA Business Associate Agreement (BAA) as a paid add-on. ## Pricing | Monthly credits | Monthly price | Annual price | | ---------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------ | | | per month | per month billed annually | | | per month | per month billed annually | | | per month | per month billed annually | | | per month | per month billed annually | | | per month | per month billed annually | Annual Team billing gives you 2 months free. ## Metered usage | Item | Team pricing | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------- | | Realtime collaboration minutes The total active time people and agents spent together inside rooms. | | | Comments created The number of comments created in rooms. | | | Realtime data storage updates Number of Liveblocks Storage row updates in the selected period, aggregated across the projects you include in the filter. | | | Realtime data stored 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. | | | 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. | | | 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. | | ## Other limits | Item | Team limit | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | | Monthly active users | | | Monthly active rooms | | | Projects | included | | Dashboard seats Team members with Owner or Member role. They can configure projects, roll keys, and more. | included, | | Simultaneous connections per room | | | Simultaneous connections per project | | | Monthly anonymous connections Public key only recommended for prototyping. Set up authentication. | | | Average connections per user | | | Active room hours per month | | | Max file upload size | | | 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 add-on | | Event log retention | | | SAML SSO | Included | | SOC 2 report | Included | | Private Slack channel | Included | | HIPAA BAA | per month Billed annually | | Environments | | ## Billing cycle The Team plan follows a monthly billing cycle that starts on the 1st of each month and ends on the last day of the month. Monthly credits renew at the beginning of each billing cycle and are applied to metered usage. Any usage not covered by credits appears on your invoice. ## Add-ons | Add-on | Price | | ------------------------- | ---------------------------------------------------------------------------------------------------- | | Unlimited Version History | per month Billed annually | | HIPAA BAA | per month Billed annually | --- 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 free monthly credits 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 developers shipping collaborative experiences in production. It includes everything in Free, but enables you to remove the "Powered by Liveblocks" badge. Pro comes with monthly credits. You can also buy the Unlimited Version History add-on. [Learn more about the Pro plan](/docs/pricing/plans/pro) ## Team The Team plan is designed for teams shipping collaborative experiences in production with the ability to scale. It includes everything in Pro, plus SAML Single Sign-on (SSO), SOC 2, and a private Slack channel. Team starts at monthly credits, and volume discounts can be applied by purchasing more credits. You can also request a HIPAA Business Associate Agreement (BAA) as a paid add-on. [Learn more about the Team plan](/docs/pricing/plans/team) ### Add-ons | Add-on | Price | | ------------------------- | --------------------------------------------------------- | | Unlimited Version History | $100 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 Team, 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 continues billing metered 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: "Agent skills" parentTitle: "Tools" description: "Use Liveblocks agent skills to help develop your Liveblocks and Yjs apps more easily." --- Create your Liveblocks applications more easily with our agent skills, designed to help AI coding assistants follow best practices for Liveblocks and Yjs. These skills use the [Agent Skills](https://agentskills.io/home) format.
## Install Install with the following command: ```bash npx skills add liveblocks/skills ``` ## Skills ### liveblocks-best-practices Best practices for using Liveblocks. Contains 40+ comprehensive references related to each feature of Liveblocks. Use it when building, debugging, or answering questions about Liveblocks. ### yjs-best-practices Best practices for using Yjs. Contains info on fixing common issues, structuring your data efficiently, and avoiding bugs. Use it when building, debugging, or answering questions about Yjs. ## Usage After installation, skills are automatically available in your AI tool. Your agent will use them when it judges they are relevant. For example, the following questions will trigger skills: ``` How do I handle unstable Wi-Fi connections with Liveblocks? ``` ``` How do I pass custom headers to my auth endpoint? ``` ``` Why is my Y.Map growing so large? ``` ## Source code The source code is available in our [GitHub repository](https://github.com/liveblocks/skills). # Liveblocks agent skills — context for your assistant **Purpose:** Help users use Liveblocks and Yjs agent skills effectively in coding assistants. **Do:** - Recommend installing with `npx skills add liveblocks/skills` when skills are missing. - Use `liveblocks-best-practices` for Liveblocks feature design/debugging questions. - Use `yjs-best-practices` for Yjs structure, performance, and bug-avoidance questions. - Encourage concrete prompts tied to the user's actual issue and stack. **Do not:** - Claim unavailable skills or undocumented behavior. - Treat skills as a replacement for project-specific verification and testing. - Assume installation succeeded without checking user feedback. Keep advice practical and tied to the two documented skills on this page. --- meta: title: "Dev server" parentTitle: "Tools" description: "Run a local Liveblocks dev server for multiplayer development and testing without hitting production." --- The Liveblocks dev server is a local server that lets you develop and test multiplayer features without connecting to Liveblocks production servers. It is built on our open-source [`@liveblocks/server`](https://github.com/liveblocks/liveblocks/tree/main/packages/liveblocks-server) package internally.
## Features The dev server fully supports all [Multiplayer](/docs/ready-made-features/multiplayer) features, and partially supports other APIs. [Learn more](#Partially-supported-features). | Feature | Support | | ---------------------------------------------------------------------------------------------------------------------------------- | ------- | | [Liveblocks Storage](/docs/ready-made-features/multiplayer/sync-engine/liveblocks-storage) (LiveObject, Presence, Broadcast, etc.) | ✅ | | [Liveblocks Yjs](/docs/ready-made-features/multiplayer/sync-engine/liveblocks-yjs) | ✅ | | [Text editors](/docs/ready-made-features/multiplayer/text-editor) (Tiptap, BlockNote, Lexical) | ✅ ¹ | | [Public key authentication](/docs/api-reference/liveblocks-react#LiveblocksProviderPublicKey) | ✅ | | [Access token authentication](/docs/authentication/access-token) | ✅ | | [ID token authentication](/docs/authentication) | ✅ | | [Room Node.js methods](/docs/api-reference/liveblocks-node#Room) | ✅ | | [Room REST APIs](/docs/api-reference/rest-api-endpoints#Room) | ✅ | | [Comments](/docs/ready-made-features/comments) | ❌ ² | | [Notifications](/docs/ready-made-features/notifications) | ❌ ² | | [AI Agents](/docs/ready-made-features/ai-agents) | ❌ | | Other APIs | ❌ | ¹ _Excluding features related to Comments and Notifications._ ² _Basic APIs will return dummy data, so you can still use the Liveblocks dev server to test other features in your app._ ## Set up the dev server The development server currently only supports connecting with [public key](/docs/api-reference/liveblocks-client#createClientPublicKey) or [access token](/docs/authentication/access-token) authentication. ID token authentication will be supported in future. Install Bun The Liveblocks dev server requires [Bun](https://bun.sh/) to be installed. ```bash npm install -g bun ``` Run the dev server Start the dev server from your project directory—it runs on [`http://localhost:1153`](http://localhost:1153). ```bash npx liveblocks dev ``` Connect to the dev server Point your client and auth endpoint to the dev server, and update your API keys. You must use `"sk_localdev"` instead of your secret key and `"pk_localdev"` instead of your public key. The easiest way to do this is to run the dev server and press `p`. It copies a prompt you can paste into your AI code editor, which will intelligently make the changes for you. If you don’t use the prompt, you can manually add `baseUrl` and change your API key: ```tsx import { LiveblocksProvider } from "@liveblocks/react"; function App() { return ( {/* Your app */} ); } ``` ```ts import { Liveblocks } from "@liveblocks/node"; const liveblocks = new Liveblocks({ // +++ baseUrl: "http://localhost:1153", secret: "sk_localdev", // +++ }); ``` ```tsx isCollapsable isCollapsed title="Alternate snippet for public key" // Only edit this file for public key import { LiveblocksProvider } from "@liveblocks/react"; function App() { return ( {/* Your app */} ); } ``` Complete You're all set up, open your app and start building collaborative features using the local dev server, or [set up CI testing](/docs/guides/how-to-set-up-continuous-integration-ci-testing) with your application. ## Partially supported features A number of unsupported features are partially implemented so you can run your app without seeing errors. For example, `useThreads` is not supported yet, but it will always return an empty array so you can still view your application. ```tsx title="React" // ✅ Fully supported const others = useOthers(); const shapes = useStorage((root) => root.shapes); // ⚠️ Returns dummy data such as empty arrays const { threads } = useThreads(); const { inboxNotifications } = useInboxNotifications(); // ⚠️ Nothing happens when calling unsupported functions const createNewThread = useCreateThread(); createNewThread({ body: {}, attachments: [] }); ``` ```ts title="Node.js" // ✅ Fully supported const storage = await liveblocks.getStorageDocument("my-room-id"); await liveblocks.broadcastEvent("my-room-id", { type: "REFRESH" }); // ⚠️ Room APIs are supported but permissions and metadata are ignored await liveblocks.createRoom("my-new-room-id", { defaultAccesses: ["room:write"], metadata: { title: "My title" }, }); // { metadata: {}, defaultAccesses: [], ... } await liveblocks.getRoom("my-new-room-id"); // ⚠️ Returns dummy data such as empty arrays const { data: threads } = await liveblocks.getThreads({ roomId: "my-room-id", }); // ⚠️ Nothing happens when calling unsupported functions await liveblocks.createThread({ roomId: "my-room-id", body: {}, }); ``` In future, we plan to support more features in the dev server. ## REST API The REST API supports the same set of features as the Node.js package. To use it, point your requests to the dev server URL and use `sk_localdev` as the secret key. ```bash curl http://localhost:1153/v2/* \ -H 'Authorization: Bearer sk_localdev' ``` ## Quick local testing When your unit tests need the Liveblocks dev server to be running, you can use `--cmd "vitest run"` (or whichever test command you use) to run your tests in a temporary, ephemeral, dev server instance. This mode will: 1. Start a fresh, empty, dev server instance. 2. Run your unit tests. 3. Stop the dev server. Example output: ```bash npx liveblocks dev --cmd 'vitest run' Starting the Liveblocks dev server... Liveblocks dev server running at http://localhost:1153 Server logs: /tmp/liveblocks-dev-XXXXXX/server.log ✓ src/__tests__/your-unit-test.test.ts (28 tests) 40ms Test Files 1 passed (1) Tests 28 passed (28) Start at 11:24:51 Duration 0.5s Liveblocks dev server shut down ``` You can also pass extra arguments after the command. They will be appended to the end of the command string: ```bash npx liveblocks dev --cmd 'vitest run' my-test.test.ts # Runs: vitest run my-test.test.ts ``` If you need precise control over where the extra arguments are inserted, use `{}` as a placeholder: ```bash npx liveblocks dev --cmd 'vitest run {} --reporter=verbose' my-test.test.ts # Runs: vitest run my-test.test.ts --reporter=verbose ``` Use `--` before any extra arguments that start with `-`: ```bash npx liveblocks dev --cmd 'vitest run' -- --coverage my-test.test.ts # Runs: vitest run --coverage my-test.test.ts ``` We recommend: 1. `npx liveblocks dev` for manual testing and development, which retains data between runs in the `.liveblocks/` folder. 2. `npx liveblocks dev --cmd "vitest run"` for locally running your unit tests against a fresh instance (your local data in `.liveblocks/` won't be affected by your unit tests). ## Continuous Integration (CI) testing You can use the dev server in CI environments by setting up a test environment file and running the dev server as a service container. Learn how do this in our step-by-step guide. ## End-to-End (E2E) testing You can use the dev server to run end-to-end tests with tools such as Playwright. Learn how to do this in our step-by-step guide. ## Docker You can run the dev server as a Docker container, which is useful for CI environments where you may not have Bun installed. ```bash docker run -p 1153:1153 ghcr.io/liveblocks/cli dev ``` To persist data between container restarts, mount a volume: ```bash docker run -p 1153:1153 -v liveblocks-data:/app/.liveblocks ghcr.io/liveblocks/cli dev ``` ## Source code The source code is available in our [GitHub repository](https://github.com/liveblocks/liveblocks/tree/main/tools/liveblocks-cli). ## Examples The following examples fully support the dev server. --- meta: title: "DevTools" parentTitle: "Tools" description: "Learn how to leverage Liveblocks DevTools to inspect and troubleshoot your collaborative application." --- Our [DevTools](/devtools) is a browser-based extension that integrates with Liveblocks and your local development environment. This allows you to easily inspect, visualize, and troubleshoot your collaborative online experiences. Our DevTools is currently only available if you are on Liveblocks 1.0 or later. To view step-by-step instructions on how to upgrade your project to Liveblocks 1.0, check out our [upgrade guides](/docs/platform/upgrading/). If you want to see how this works in action, check out this [live demo](https://www.youtube.com/watch?v=-6Jxejp0iuw) recorded by [Vincent](https://twitter.com/nvie) on YouTube. ## Install the browser extension Our DevTools extension is available for all major web browsers: - [Chrome](https://chrome.google.com/webstore/detail/liveblocks-devtools/iiagocfmmhknpdalddkbiejnfmbmlffk) - [Firefox](https://addons.mozilla.org/en-US/firefox/addon/liveblocks-devtools/) - [Edge](https://microsoftedge.microsoft.com/addons/detail/liveblocks-devtools/hfecmmnilleegmjaegkjjklnjbgadikg) Within the “Liveblocks” panel, you will see two panes: storage and presence. The storage pane represents each object that you have defined in storage as a tree, likewise, the presence pane represents each user that is connected to your Liveblocks project. ## Trying our DevTools ### With our Starter Kit The [Liveblocks Starter Kit](/starter-kit) is a great way to get started with Liveblocks and learn how to use our DevTools.
To use the Starter Kit with DevTools, run the following command: ```bash npx create-liveblocks-app@latest --next ``` You can then follow the guided prompts in the terminal to create your project. For additional information on how to use the Starter Kit, see the [Starter Kit guide](/docs/tools/nextjs-starter-kit). Once you have completed the Starter Kit prompts and installed the extension, run the Starter Kit with `npm run dev` and open your browser to `localhost:3000`. You should authenticate, and then create a new draft document. Once you have created a new document, open up the developer tools window. Find the newly available “Liveblocks” panel there.
### With an example from our gallery All of [our examples](/examples) hosted on Liveblocks.io are available for you to run locally from the [examples directory](https://github.com/liveblocks/liveblocks/tree/main/examples) in the Liveblocks repo. For testing and reviewing how to use our DevTools, we will use the [Advanced Collaborative Spreadsheet](https://liveblocks.io/examples/collaborative-spreadsheet-advanced/nextjs). To download this example, run the following command, and follow the guided prompts to create your project: ```bash npx create-liveblocks-app@latest --example nextjs-spreadsheet-advanced ``` Once you have completed the prompts and installed the extension, run the example with `npm run dev` and open your browser to `localhost:3000`.
Liveblocks DevTools panel
Open your browser and open up the developer tools window. Find the newly available “Liveblocks” panel there, and you should see the values from the spreadsheet examples populating storage. ## Features and tips ### Highlighting When a value in storage changes, the DevTools will highlight the value to indicate that it has changed.
### Search Storage values are searchable by key and regex.
### Hover Hovering over a value in storage or presence makes an eye icon appear. If clicked, it displays the value of that key.
### Expand and collapse - You can expand and collapse objects with one level of depth by using the arrow keys or spacebar - You can quickly expand and collapse nested objects within storage by holding the alt/option key and clicking on the object If you open the DevTools after a Liveblocks client has already been initialized, you may see an empty page. This is because our DevTools is only able to retrieve the current state of storage and presence. To fix this, you can refresh the page or open the DevTools before initializing the Liveblocks client. --- meta: title: "MCP server" parentTitle: "Tools" description: "Learn to use the official MCP server for Liveblocks, helpful for inspecting and modifying your project with AI in Cursor, VS Code, Claude Desktop, and more." --- The Liveblocks MCP server allows you to inspect and modify your project with AI in compatible apps such as Cursor, Claude, Codex, and more.
## Features Our MCP server features 39 different tools, enabling most features from our [REST API](/docs/api-reference/rest-api-endpoints). You can use it to: - Create and modify rooms, threads, comments, notifications, more. - Read realtime Storage and Yjs values. - Broadcast custom events to connected clients. - Mark threads and notifications as read/unread. AI will often string tool calls together—for example, if you ask it to fetch a room that doesn’t exist, it will ask if you’d like to create the room first, and then fetch it after. ## Setup To install, you need to use your secret key from the correct project in [your dashboard](/dashboard). Below are instructions for [Cursor](#Cursor), [Claude Code](#Claude-Code), [Claude Desktop](#Claude-Desktop), and [Codex](#Codex). Don’t insert a secret key from your production application, as AI will have direct access to modify it. ### Cursor To set up [Cursor](https://cursor.sh/): 1. Go to File → Cursor Settings → MCP → Add new server. 2. Add the following: ```json { "mcpServers": { "liveblocks": { "command": "npx", "args": ["-y", "github:liveblocks/liveblocks-mcp-server"], "env": { "LIVEBLOCKS_SECRET_KEY": "{{SECRET_KEY}}" } } } } ``` ### Claude Code To set up [Claude Code](https://claude.ai/code), run the following command in the terminal: ```bash claude mcp add liveblocks -e LIVEBLOCKS_SECRET_KEY="{{SECRET_KEY}}" -- npx -y github:liveblocks/liveblocks-mcp-server ``` ### Claude Desktop To set up [Claude Desktop](https://claude.ai/download): 1. Go to Settings → Developer → Edit Config. 2. Open the JSON file, `claude_desktop_config.json`. 3. Add the following to the JSON: ```json { "mcpServers": { "liveblocks": { "command": "npx", "args": ["-y", "github:liveblocks/liveblocks-mcp-server"], "env": { "LIVEBLOCKS_SECRET_KEY": "{{SECRET_KEY}}" } } } } ``` ### Codex To set up [Codex](https://codex.com/): 1. Ensure the Codex CLI is installed: ```bash npm i -g @openai/codex ``` 2. Run the following command in the terminal, inserting your secret key: ```bash codex mcp add liveblocks \ --env LIVEBLOCKS_SECRET_KEY="{{SECRET_KEY}}" \ -- npx -y github:liveblocks/liveblocks-mcp-server ``` ## Source code The source code is available in our [GitHub repository](https://github.com/liveblocks/liveblocks-mcp-server). # Liveblocks MCP server — context for your assistant **Purpose:** Help users correctly install and use the official Liveblocks MCP server in AI tools. **Do:** - Ask which client they use (Cursor, VS Code, Claude Desktop, or other) and provide the matching setup path. - Emphasize using a secret key from the correct Liveblocks project and verifying environment names. - Recommend development-only usage for MCP access and safer project separation when possible. - Point users to dashboard key management and the official MCP source/docs when needed. **Do not:** - Recommend reusing production application secrets casually in local AI tooling. - Invent MCP tools, configuration fields, or capabilities not documented. - Skip key safety warnings when giving copy/paste setup commands. Adapt guidance to the user's editor/client and project setup details. --- meta: title: "Next.js Starter Kit" parentTitle: "Tools" description: "Kickstart your collaborative SaaS product with the Liveblocks Next.js Starter Kit." --- The Next.js Starter Kit is an open-source template that showcases all aspects of Liveblocks in a single, modern application.
If you’d like to see how this works, make sure to [check out this live demo video](https://www.youtube.com/watch?v=Lf7HQ4Z_Ovc) on YouTube, or [try the project online](https://nextjs-starter-kit.liveblocks.app). ## Features The Next.js Starter Kit includes the following - Documents dashboard with pagination, organizations, auto-revalidation. - Collaborative apps including a whiteboard, text documents, note editor, drawing canvas. - Realtime presence with avatars, cursors, carets. - Fully-featured share menu with users, organizations, public permissions. - Authentication compatible with GitHub, Google, Auth0, and more.
## Set up the Next.js Starter Kit ### create-liveblocks-app You can get started by running the following command: ```bash npx create-liveblocks-app@latest --next ``` This will run an installer that allows you to download, configure, and deploy, your project:
Give your project a name, and select the [authentication](#authentication) method you’d like to use in your app. If you are trying to generate a proof of concept quickly, opting to use demo authentication may be a great option—you can still add other authentication providers later on. #### Deploy with Vercel Integration If you would like to set up CI/CD with your application, we’ve made that process straightforward for you as well. When prompted, you can select "deploy to Vercel" to enable building and deploying the starter kit. The Vercel Integration will open in a new browser window for you to complete the process of adding your repository.
If by clicking "Create" you receive "An unexpected internal error occurred" you should validate within your code hosting platform of choice (GitHub, GitLab, Bitbucket) that the Vercel integration has permission to access your repository. In GitHub, permissions can be found can be found under Settings > Integrations > Vercel. #### Connect to Liveblocks and retrieve your secret key If you prefer to work locally, you can tell the installer you would not like to "deploy to Vercel". After declining the deployment option, the installer will prompt adding your Liveblocks Key to the application automatically. If you forwent deployment or indicated that you would like to get your Liveblocks secret key automatically, the Liveblocks integration page will open in a new browser tab. Once the integration page appears, you can sign up/sign in to Liveblocks, create a new project, and import your API key into the installer.
After finishing up, check the installer and follow the commands recommended to get you started. ### Authentication [#authentication] The Liveblocks starter kit uses [NextAuth.js](https://next-auth.js.org/) for authentication, meaning many authentication providers can be configured with minimal code changes. A demo authentication system is used by default, but it’s easy to add real providers, such as GitHub, Auth0, and more. Take a look at the guide for your chosen authentication method: - [GitHub authentication](#github-authentication) - [Auth0 authentication](#auth0-authentication) - [Demo authentication](#demo-authentication) ### GitHub authentication [#github-authentication] To use GitHub auth, make sure you selected "GitHub authentication" when running the installer (or you’ve set up the [provider manually](#add-multiple-authentication-providers)). This is how to set up your GitHub secret key and client id. 1. Go to [Developer Settings](https://github.com/settings/apps) on GitHub and click "New GitHub App". 2. Enter an app name (e.g. `Liveblocks Starter Kit (dev)`). You’ll need a new app for each environment, so it’s helpful to place "dev" in the name. 3. Add a homepage URL—this isn’t important now, so a placeholder will do. 4. Find the "Callback URL" input just below, and add your local development URL (e.g. `http://localhost:3000`). 5. Look for the "Webhook" section and make sure to uncheck "Active". 6. Use the remaining default settings and press "Create GitHub App".
7. On the next page under "Client secrets", press the "Generate a new client secret" button (note that this is different to generating a private key!). 8. Copy this secret into the `/.env.local` file as `GITHUB_CLIENT_SECRET` 9. Go back to the previous page and find the "Client ID" near the top. Copy this into your `.env.local` file as `GITHUB_CLIENT_ID` Almost there! `.env.local` should now contain lines similar to this: ```env file=".env.local" GITHUB_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX GITHUB_CLIENT_ID=XXXXXXXXXXXXXXXXXXXX ``` GitHub authentication is now complete! Next, [add yourself as a user](#how-to-sign-up) to test out your authentication. ### Auth0 authentication [#auth0-authentication] To use Auth0 auth, make sure you selected "Auth0 authentication" when running the installer (or you’ve set up the [provider manually](#add-multiple-authentication-providers)). This is how to set up your Auth0 secret key and client information. 1. Go to your [Auth0 Dashboard](https://manage.auth0.com/dashboard) and click "Create Application". 2. Enter an app name (e.g. `Liveblocks Starter Kit (dev)`). You’ll need a new app for each environment, so it’s helpful to place "dev" in the name. 3. Select "Single Page Web Applications", and press "Create". 4. Copy your "Client ID" from the top of the page, and place it within `.env.local` as `AUTH0_CLIENT_ID`. 5. Click the "Settings" tab— we’ll be making a number of changes here. 6. Find the "Client Secret" input field, and copy the value into `.env.local` as `AUTH0_CLIENT_SECRET`. 7. Copy your "Domain" from the input field, add "https://" to the start, and place it within `.env.local` as `AUTH0_ISSUER_BASE_URL`. 8. Add the following to the "Allowed Callback URLs" textarea: `http://localhost:3000/api/auth/callback/auth0`. 9. Add the following to the "Allowed Logout URLs" textarea: `http://localhost:3000`. 10. Scroll to the bottom and press "Save changes".
`.env.local` should now contain these three lines, along with anything previously there: ```env file=".env.local" AUTH0_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX AUTH0_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX AUTH0_ISSUER_BASE_URL=https://XXXXXXXXXXXXXXXXXX.com ``` Auth0 authentication is now set up! Next, [add yourself a user](#how-to-sign-up) to test out your authentication. ### Demo authentication [#demo-authentication] For quickly testing out your app, the demo authentication method can be used. This method uses a NextAuth [`CredentialsProvider`](https://next-auth.js.org/providers/credentials) to simulate a real sign in system. To replace it with your own authentication method, you can add any other NextAuth [`Provider`](https://next-auth.js.org/providers/) to [`/auth.config.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/auth.config.ts). Read the [next section](#how-to-sign-up) to learn how to add a new user to your demo application. ### How to sign up - add yourself as a user [#how-to-sign-up] We haven’t set up a database, so we’re temporarily using the [`/data`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/data) folder instead. Before any user can sign in, they need to be added to [`/data/users.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/data/users.ts). Navigate there and add your details, for example, if you’re signing in with `yourname@example.com`: ```ts file="data/users.ts" { id: "your.name@example.com", name: "Your Name", avatar: "https://liveblocks.io/avatars/avatar-0.png", organizationIds: ["liveblocks", "your.name@example.com"], }, ``` Note that this is replacing the sign-up process, so you must enter the email of any new user. If, for example, you’re using GitHub authentication, you can enter the email address of any valid GitHub account. ### Ready to go The Next.js Starter Kit is now ready to use! After setting up authentication, make sure to restart the dev server to see your authentication in action. ```bash npm run dev ``` ## Assorted info ### Structure The [starter kit](https://github.com/liveblocks/liveblocks/tree/main/starter-kits/nextjs-starter-kit) has the following structure: ``` app components data icons layouts lib └─ actions └─ database └─ hooks └─ utils primitives styles types utils auth.ts auth.config.ts constants.ts liveblocks.config.ts liveblocks.server.config.ts package.json ``` The important thing to note is that there are primitives, which are base level components such as buttons (think of these as the small building blocks of a page), components, which you can think of as multiplayer implementations (cursors, badges, etc), app, which renders document level experiences and a library (lib) of both server side and client side methods which contain the logic to create and modify documents.
More info about the structure
`/app/`
Next.js app folder.
`/components/`
Components used in the app.
`/data/`
A demo database.
`/icons/`
Every icon component.
`/layouts/`
Page layout components.
`/lib/actions/`
Server actions that can be used on both client and server, used for accessing and modifying documents.
`/lib/database/`
Server functions used for getting database information.
`/lib/hooks/`
A set of React hooks that are used on the client.
`/lib/utils/`
Assorted document-related utilities.
`/primitives/`
Generic reusable components.
`/types/`
All reusable TypeScript types.
`/auth.ts`
File that links NextAuth to your database.
`/auth.config.ts`
File where NextAuth providers are set up.
`/constants.ts`
Constant strings used in the app, such as URLs and global IDs.
`/liveblocks.config.ts`
Liveblocks config file.
`/liveblocks.server.config.ts`
Server-side Liveblocks config file.
### Async fetching and error handling This starter kit makes extensive use of the following programming pattern for fetching async resources: ```ts const { data, error } = await getDocument({ /* ... */ }); // An error has occured if (error) { // { code: 400, message: "Document not found", suggestion: "Please check the URL is..." } console.log(error); return; } // Success, but result is empty if (!data) { return; } // Success // { name: "my-document", id: "hIas7GuihgHF8Fhv8Sskg", ... } console.log(data); ``` ### Document functions Much of the starter kit’s power is in the [`/lib/actions`](https://github.com/liveblocks/liveblocks/tree/main/starter-kits/nextjs-starter-kit/lib/actions) directory. The functions in these files allow you to edit your documents easily and return type-safe objects. For example in an API endpoint: ```ts import { createDocument } from "@/lib/actions"; export async function POST() { // Create a new document const { data, error } = await createDocument({ name: "My document", type: "whiteboard", userId: "charlie.layne@example.com", }); // ... } ``` Because these functions are server actions, they can be leveraged on both client and server. ```tsx import { createDocument } from "@/lib/actions"; export function CreateDocumentButton() { async function handleCreateDocument() { // Create a new document const { data, error } = await createDocument({ name: "My document", type: "whiteboard", userId: "charlie.layne@example.com", }); } return ; } ``` Functions that return data can be used with [SWR hooks](https://swr.vercel.app/) that automatically update your data in components. For example, `getDocumentUsers` returns a list of users with access to the room: ```tsx // Convert from this const { data, error } = await getDocumentUsers({ documentId: "my-document-id", }); // To this const { data, error } = useDocumentsFunctionSWR([ getDocumentUsers, { documentId: "my-document-id", }, ]); ``` Here’s a working example: ```tsx import { getDocumentUsers, useDocumentsFunctionSWR } from "../../lib/client"; export function ListUsers() { // Get users attached to a document and update every 1000ms const { data: users, error: usersError } = useDocumentsFunctionSWR( [ getDocumentUsers, { documentId: "my-document-id", }, ], { refreshInterval: 1000 } ); if (usersError) { return
Error
; } if (!users) { return
Loading...
; } return (
    {users.map((user) => (
  • user.name
  • ))}
); } ``` ### How to extend the Document type If you’d like to add a new property to `Document`, it’s simple. First, edit the `Document` type in [`/types/documents.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/types/document.ts): ```ts file="types/documents.ts" export type Document = { // Your new property randomNumber: number; //... }; ``` Then modify the return value in [`/lib/utils/buildDocuments.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/utils/buildDocuments.ts). This is a function that converts a Liveblocks room into your custom document format: ```ts file="lib/utils/buildDocuments.ts" // Return our custom Document format const document: Document = { randomNumber: Math.random(), // ... }; ``` Next, run the following command to check for problems: ```bash npm run typecheck ``` If no errors are returned, the document properties were successfully extended. ### How to extend the User & Session type Similar to the way we extend the `Document` type, we can also extend the `User` and `Session` type. Adding a new property to `User`/`Session` is simple. First, edit the `User` type in [`/types/data.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/types/data.ts). ```ts file="types/data.ts" export type User = { // Your new property randomNumber: number; // ... }; ``` Then make sure to return this new property in [`/lib/database/getUser.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/database/getUser.ts). ```ts file="lib/database/getUser.ts" return { randomNumber: Math.random() /* ... */ }; ``` The new property will now be available to use in your app: ```ts // randomNumber: Math.random(), console.log(session.user.info.randomNumber); ``` #### Adding this to your Liveblocks app (optional) Liveblocks presence is a way of displaying online presence between users, helpful for live avatars, realtime cursors, etc., and it’s possible to attach a properties to it for each user. To make a property accessible in presence (and within the React hooks used in whiteboard), you must modify `UserInfo` in [`/liveblocks.config.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/liveblocks.config.ts). ```ts file="liveblocks.config.ts" export type UserInfo = Pick; ``` In this example [Pick](https://www.typescriptlang.org/docs/handbook/utility-types.html) creates the UserInfo type based off of the User type and adds additional keys based on the properties you provide. After this, modify [`/lib/actions/authorizeLiveblocks.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/actions/authorizeLiveblocks.ts). First, we’ll give an anonymous user a property: ```ts file="lib/actions/authorizeLiveblocks.ts" // Anonymous user info const anonymousUser: User = { randomNumber: Math.random(), // ... }; ``` Next, we’ll get the signed-in user’s property: ```ts file="lib/actions/authorizeLiveblocks.ts" // Get current user info from session (defined in /auth.config.ts) // If no session found, this is a logged out/anonymous user const { randomNumber, // ... } = session?.user.info ?? anonymousUser; ``` And then pass this info to `authorize`: ```ts file="lib/actions/authorizeLiveblocks.ts" // Get Liveblocks access token const { data, error } = await authorize({ userInfo: { randomNumber /* ... */ }, // ... }); ``` To make sure to check everything’s hooked up correctly: ```bash npm run typecheck ``` Once that’s working, the new property can then be used in your app: ```tsx // My random number const myRandomNumber = useSelf((me) => me.info.randomNumber); // An array of everyone else’s random numbers const everyonesRandomNumbers = useOthersMapped( (other) => other.info.randomNumber ); ``` ### Adding multiple authentication providers [#add-multiple-authentication-providers] It’s possible to add multiple authentication providers to the starter kit using [NextAuth Providers](https://next-auth.js.org/providers/). Open [`/auth.config.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/auth.config.ts) and place your providers in the object: ```ts import GithubProvider from "next-auth/providers/github"; import Auth0Provider from "next-auth/providers/auth0"; export const authOptions = { // ... providers: { GithubProvider({ clientId: process.env.GITHUB_CLIENT_ID as string, clientSecret: process.env.GITHUB_CLIENT_SECRET as string, }), Auth0Provider({ clientId: process.env.AUTH0_CLIENT_ID as string, clientSecret: process.env.AUTH0_CLIENT_SECRET as string, issuer: process.env.AUTH0_ISSUER_BASE_URL as string, }), } }; ``` It’s not only possible with GitHub and Auth0, any [NextAuth provider](https://next-auth.js.org/providers/) will work, such as Google, X, Reddit, or more. You can find more information about getting the necessary secrets on the NextAuth documentation, or on the provider’s website. Note that if you’re using `CredentialsProvider` (for example, as used in the demo authentication), `CredentialsProvider` must be removed before any other authentication methods will appear. ### Switching themes The starter kit comes with both a dark mode and light mode. By default, the user sees the theme that corresponds to their system setting, but it’s easy to switch your whole app to just dark or light mode by modifying [`styles/globals.css`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/styles/globals.css). - To use only light mode, remove the entire `@media (prefers-color-scheme: dark)` media query. - To use only dark mode, copy the "Dark mode" CSS variables into the "Light mode" section, then remove the entire `@media (prefers-color-scheme: dark)` media query. ### Adding a database To add a database you need to modify the following async functions to return the correct properties: - [`/lib/database/getGroup.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/database/getGroup.ts) - [`/lib/database/getGroups.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/database/getGroups.ts) - [`/lib/database/getOrganization.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/database/getOrganization.ts) - [`/lib/database/getUser.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/database/getUser.ts) You can then remove the `/data` folder. Everything else should work as expected. --- meta: title: "Upgrading to 0.17" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 0.17" --- ```bash npm install @liveblocks/client@0.17 @liveblocks/react@0.17 ``` With the release of 0.17 we’re making a big investment in the stability and reliability of Liveblocks. Our long term goal is to empower you to write and help evolve your apps in the best way possible, and at enterprise scale. It already was easy to get started with Liveblocks, and with these changes we want to make _evolving_ your app just as easy. The first step towards this goal is to take TypeScript support to the next level for [`@liveblocks/client`][] and [`@liveblocks/react`][] (but our other packages will soon follow suit). This will help you write code with confidence and catch bugs as soon as possible in the development process. We have strictened our type definitions to be more accurate and will recommend some new usage patterns, so it will be easier for you to create _bug free_ collaborative apps. To upgrade [`@liveblocks/client`][] and [`@liveblocks/react`][], run the following command. ```bash npm install @liveblocks/client@0.17 @liveblocks/react@0.17 ``` With these changes, we’re clearing the path to enable schema validation per room, automatic data migrations, more powerful data selector APIs, and other enterprise-level features. Let’s dive in and take a look! ## Changes in @liveblocks/react [#react] ### Lifting up your state to the room level [#lifting-state-to-room] With 0.16, it was possible to initialize a storage key with [`useObject`][], [`useList`][], [`useMap`][]. Even if handy, we realized that it introduced confusion and unpredictable behavior for most users. Imagine a scenario where you have two components initializing the same storage key. ```tsx function ComponentA() { const author = useObject("author", { firstName: "Ada", lastName: "Lovelace", }); /* ... */ } function ComponentB() { const author = useObject("author", { firstName: "Margaret", lastName: "Hamilton", }); /* ... */ } ``` Depending on which component renders first, `author` will be Margaret Hamilton or Ada Lovelace. To make this more predictable, we’re deprecating this and recommend initializing the storage at the [`RoomProvider`][] level. #### Before ❌ [@hidden] ```tsx function ComponentA() { const author = useObject( "author", // ⚠️ Don’t initialize your data here anymore! { firstName: "Ada", lastName: "Lovelace" } ); /* ... */ } ``` #### After ✅ [@hidden] ```tsx highlight="5-10,13,20" import { LiveObject } from "@liveblocks/client"; function Root() { // Instead, initialize it at the RoomProvider level to remove all ambiguity const initialStorage = { author: new LiveObject({ firstName: "Ada", lastName: "Lovelace", }), }; return ( ); } function ComponentA() { const author = useObject("author"); /* ... */ } ``` Notice the explicit use of LiveObject here now. Before, when using useObject (or useList, useMap), the data you passed to initialize it would automatically get wrapped in LiveObject for you. This seemed convenient at first, but it also caused confusion about which Storage keys were Live structures and which ones were “normal” values. By opting in to which keys you want to be Live structures, you now get full control over your storage data. If you run into issues with these new patterns and you need help, please [let us know](https://github.com/liveblocks/liveblocks/discussions/359). We’re here to help! ### A better way to annotate your own types [#react-better-type-annotations] In 0.16, most of our hooks accepted generic parameters that let you explicitly provide your own types. For example: ```tsx highlight="3-5,11,19" import { useMyPresence, RoomProvider } from "@liveblocks/react"; type Presence = { cursor: { x: number; y: number }; }; function Root() { return ( ); } function Component() { const [myPresence] = useMyPresence(); const cursor = myPresence.cursor; // Valid /* ... */ } ``` One issue with this API was that there was no good way to make sure that `RoomProvider.initialPresence` and [`useMyPresence`][] types remain synchronized, as there was no inherent connection between these. If we added a `color` property to the `Presence` type, it would still be missing from the `initialPresence` at the `RoomProvider` level, and TypeScript would not be able to catch that bug. The opposite would also fail; omitting the `cursor` property on the `initialPresence` would break at runtime but TypeScript would not be able to catch this issue for you! Another issue is that there could be many places where you’d have to provide those extra type annotations. With 0.17, we’re fixing all of this! To do so, we’re introducing a new API called [`createRoomContext`][]. It lets you type your `RoomProvider` and make sure that all your hooks types are synchronized with the `RoomProvider`. Besides this initial setup, you will no longer have to provide any type annotations elsewhere anymore. ```tsx import { createClient } from "@liveblocks/client"; import { createRoomContext } from "@liveblocks/react"; const client = createClient({ /* client options */ }); type Presence = { cursor: { x: number; y: number }; }; // This is just to illustrate the API - read on for tips on where to put this! const { RoomProvider, useMyPresence } = createRoomContext(client); function Root() { return ( ); } function Component() { const [myPresence] = useMyPresence(); // We can now be sure that cursor is a valid property without any generic typed param const cursor = myPresence.cursor; /* ... */ } ``` As you can see, `createRoomContext` optionally takes type parameters that let you specify the shape of your app’s data (by specifying your own `Presence`, `Storage`, `UserMeta`, `Event` types). Depending on the complexity of your app, you may only need to use one or more of these. Take a look at these examples to better see how to use and configure it: - Our live-cursors-chat example app uses [`Presence`](https://github.com/liveblocks/liveblocks/blob/a11e5744531a21201ae9f9f9ad4f1aa5e74e141e/examples/nextjs-live-cursors-chat/liveblocks.config.ts#L8-L25) only - Our live-avatars example app uses [`UserMeta`](https://github.com/liveblocks/liveblocks/blob/a11e5744531a21201ae9f9f9ad4f1aa5e74e141e/examples/nextjs-live-avatars/liveblocks.config.ts#L8-L26) only - Our logo-builder example app uses [`Presence`](https://github.com/liveblocks/liveblocks/blob/a11e5744531a21201ae9f9f9ad4f1aa5e74e141e/examples/nextjs-logo-builder/liveblocks.config.ts#L8-L31), [`Storage`](https://github.com/liveblocks/liveblocks/blob/a11e5744531a21201ae9f9f9ad4f1aa5e74e141e/examples/nextjs-logo-builder/liveblocks.config.ts#L8-L31), and [`UserMeta`](https://github.com/liveblocks/liveblocks/blob/a11e5744531a21201ae9f9f9ad4f1aa5e74e141e/examples/nextjs-logo-builder/liveblocks.config.ts#L8-L31) ### Recommended upgrade steps To make this refactoring as easy as possible, follow the steps below. #### Step 1 - Upgrade `@liveblocks/client` and `@liveblocks/react` [@hidden] To upgrade [`@liveblocks/client`][] and [`@liveblocks/react`], run the following command. ```bash npm install @liveblocks/client@0.17 @liveblocks/react@0.17 ``` #### Step 2 - Create a new file called `liveblocks.config.ts` where you will create your Liveblocks client, provider and hooks and re-export them. [@hidden] ```tsx file="liveblocks.config.ts" import { createClient } from "@liveblocks/client"; import { createRoomContext } from "@liveblocks/react"; const client = createClient({ /* client options */ }); // Presence represents the properties that will 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 = { // author: LiveObject<{ firstName: string, lastName: string }>, // ... }; // Optionally, UserMeta represents static/readonly metadata on each User, as // provided by your own custom auth backend (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 broadcasted and listened for in this // room. Must be JSON-serializable. // type RoomEvent = {}; export const { RoomProvider, useMyPresence, useObject, /* ...all the other hooks you’re using... */ } = createRoomContext(client); ``` #### Step 3 - Replace all the direct hook imports from `@liveblocks/react` by your path to `liveblocks.config.ts` and remove all generic params. [@hidden] ##### Before ❌ [@hidden] ```tsx import { useMyPresence, useOthers, useObject } from "@liveblocks/react"; import { Author } from "./types"; type MyPresence = { cursor: { x: number; y: number } | null; }; function Component() { const author = useObject("author"); const [{ cursor }] = useMyPresence(); const others = useOthers(); } ``` ##### After ✅ [@hidden] ```tsx import { useMyPresence, useOthers, useObject } from "./liveblocks.config"; function Component() { const author = useObject("author"); const [{ cursor }] = useMyPresence(); const others = useOthers(); } ``` #### Step 4 - Remove your [`LiveblocksProvider`][] at the top of your react tree. It’s not needed anymore! [@hidden] ##### Before ❌ [@hidden] ```tsx import { createClient } from "@liveblocks/client"; import { LiveblocksProvider, RoomProvider } from "@liveblocks/react"; const client = createClient({ /* ... */ }); ReactDOM.render( , document.getElementById("root") ); ``` ##### After ✅ [@hidden] ```tsx import { RoomProvider } from "./liveblocks.config"; ReactDOM.render( , document.getElementById("root") ); ``` If you run into issues with these new patterns and you need help, please [let us know](https://github.com/liveblocks/liveblocks/discussions/360). We’re here to help! ## Changes in @liveblocks/client [#client] ### Removed dangerous default type params [#no-more-default-type-params] In 0.16, while [`LiveList`][], [`LiveMap`][], [`LiveObject`][] were generics, they also took default type params, which made them a footgun. It was easy to accidentally use them in a way that would discard useful type information and hinder inference. For example: ```ts // ✅ Inferred let list = new LiveList([1, 2, 3]); // ✅ Explicit let list: LiveList = new LiveList([1, 2, 3]); // ☢️ 0.16: Dangerous footgun: discards useful type information! // 🚫 0.17: No longer possible let list: LiveList = new LiveList([1, 2, 3]); ``` They now mimic their equivalent built-in TypeScript generics, so: - `LiveList` is now just like `Array` - `LiveMap` is now just like `Map` - `LiveObject<{ a: number, b: string }>` is now just like `{ a: number, b: string }` ### No longer import Presence [#define-dont-import-presence] In 0.16, we exposed a `Presence` type that you could import, which was just an alias for “any JSON object”—not that useful! Importing Presence from Liveblocks made no sense. By definition, Presence is data owned and defined by your application after all. You should no longer need to _import_ this type. ```tsx // ❌ No longer need to _import_ Presence import type { Presence } from "@liveblocks/client"; client.enter("myRoom"); ``` Instead, just _define_ it: ```tsx // ✅ Just define the shape your app needs type Presence = { cursor: { x: number; y: number } | null; }; client.enter("myRoom"); ``` ### A better way to annotate your own types [#client-better-type-annotations] We already talked about why, in React, we [improved the way you can annotate your own app’s data](#react-better-type-annotations), by annotating the types only once, at the “top” of your app. For the same reason, we’re doing a similar thing in the client package. This section only applies if you’re not using the React package. ```tsx highlight="18-21,24" import { createClient } from "@liveblocks/client"; import { Author } from "./types"; const client = createClient({ /* client options */ }); type Presence = { cursor: { x: number; y: number } | null; }; type Storage = { author: LiveObject; }; // ❌ In 0.16, you had to annotate each method separately const room = client.enter("myRoom"); const { root } = await room.getStorage(); const author = root.get("author"); const me = room.getPresence(); const others = room.getOthers(); // ✅ In 0.17, you can simply annotate it once, at the "top" const room = client.enter("myRoom"); const { root } = await room.getStorage(); const author = root.get("author"); const me = room.getPresence(); const others = room.getOthers(); ``` If you run into issues with these new patterns and you need help, please let us know. We’re here to help! [`@liveblocks/client`]: /docs/api-reference/liveblocks-client [`@liveblocks/react`]: /docs/api-reference/liveblocks-react [`createroomcontext`]: /docs/api-reference/liveblocks-react#createRoomContext [`liveblocksprovider`]: /docs/api-reference/liveblocks-react#LiveblocksProvider [`livelist`]: /docs/api-reference/liveblocks-client#LiveList [`livemap`]: /docs/api-reference/liveblocks-client#LiveMap [`liveobject`]: /docs/api-reference/liveblocks-client#LiveObject [`roomprovider`]: /docs/api-reference/liveblocks-react#RoomProvider [`uselist`]: /docs/api-reference/liveblocks-react#useList [`usemap`]: /docs/api-reference/liveblocks-react#useMap [`usemypresence`]: /docs/api-reference/liveblocks-react#useMyPresence [`useobject`]: /docs/api-reference/liveblocks-react#useObject --- meta: title: "Upgrading to 0.18" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 0.18" --- ```bash npm install @liveblocks/client@0.18 @liveblocks/react@0.18 ``` With the release of 0.18 we’re bringing some exciting and pretty major improvements to our React hooks, letting you build apps with ease and with more control over the exact behavior. The new APIs we’re introducing here solve many subtle and not-so-subtle pain points. We heard your feedback, and think we have shipped something awesome that you’ll love. This guide consists of two sections: 1. Introduction of the new APIs to get a taste of the new features 2. Recommended upgrade steps to get the most out of it for your app ([jump straight to it](#upgrade-steps)) Let’s dive right in! ## Changes in @liveblocks/react [#react] With 0.18, the biggest conceptual shift is that our hooks to consume data from Liveblocks now return normal JavaScript data structures (objects, arrays, maps) that are _immutable_ by default. Suppose you have initialized your room with: ```tsx highlight="5-9" ``` ### Accessing nested data [#nested-data] Reading nested data from there is now much easier: #### Previously ❌ [@hidden] ```tsx function Component() { const scientist = useObject("scientist"); if (scientist == null) { return null; } const pets = scientist.get("pets").toArray(); // ["🐶", "🐈"] } ``` #### Now ✅ [@hidden] ```tsx function Component() { const pets = useStorage((root) => root.scientist.pets); // ["🐶", "🐈"] } ``` As you can see, because we can read data with normal JavaScript data structures, accessing nested data is now straightforward. ### Subscribing to updates is automatic now [#subscriptions] Rerendering your components when nested data—like our scientist’s pets list—changes was a true head breaker before. This required extra helper components, manual subscriptions, and manual conversion to “normal” JavaScript arrays. Now, rerendering your component when data changes is automatic, even for deeply nested data. #### Previously ❌ [@hidden] ```tsx function Component() { const scientist = useObject("scientist"); if (scientist == null) { return null; } return ; } function Pets({ livePets }) { const room = useRoom(); const [pets, setPets] = useState(livePets.toArray()); useEffect(() => { return room.subscribe(livePets, () => { setPets(livePets.toArray()); }); }, [room, livePets]); } ``` #### Now ✅ [@hidden] ```tsx function Component() { const pets = useStorage((root) => root.scientist.pets); // ["🐶", "🐈"] } ``` There is no typo in this example. This is the actual code. ### Multiple subscriptions are just as easy [#multi-subscriptions] Previously, if you wanted to derive a computed value from multiple storage values, it took some manual setup to ensure the component would automatically rerender when either of those values changed. Now, this is fully automatic. Or should we say, automagic? #### Previously ❌ [@hidden] ```tsx highlight="11" function Component() { const objA = useObject("a"); const objB = useObject("b"); const room = useRoom(); const [sum, setSum] = useState(); // ^^^ We’re trying to compute the result of a.x + b.x in here useEffect(() => { function onChange() { setSum(objA.get("x") + objB.get("x")); } const unsubA = room.subscribe(objA, onChange); const unsubB = room.subscribe(objB, onChange); return () => { unsubA(); unsubB(); }; }, [room, objA, objB]); } ``` #### Now ✅ [@hidden] ```tsx function Component() { const sum = useStorage((root) => root.a.x + root.b.x); } ``` This component will rerender automatically any time `a.x` or `b.x` changes, but not more often. ### Guaranteed referential equality [#referential-equality] Previously we returned mutable Live structures for performance reasons because converting live changing data to JavaScript data structures constantly (and recursively) was previously too slow to do on every render. This led to unintuitive behavior when used with React hooks dependencies. Not anymore! Due to a technique called structural sharing, we’re now able to _guarantee_ for nodes in the Storage tree that as long as their (direct or nested) contents haven’t changed in Storage, their immutable representation will remain to be the same object references on the next render. This means that you can rely on referential equality, as you may have expected in the first place. #### Previously ❌ [@hidden] ```tsx function Component() { const scientist = useObject("scientist"); useEffect(() => { // Effect never triggered when scientist (or their pets list) changes! :( }, [scientist]); } ``` #### Now ✅ [@hidden] ```tsx function Component() { const scientist = useStorage((root) => root.scientist); useEffect(() => { // Effect triggered every time scientist (or their pets list) changes! :) // But not more often than that! }, [scientist]); } ``` ### Suspense support Starting with 0.18, all hooks that read data from Liveblocks come with a Suspense version of the hook which will never return `null` to indicate the “still loading” state. Instead, they will suspend the rendering of the component tree until Liveblocks has finished loading. We recommend you to adopt Suspense if you can because it lets you get rid of the ugly `null` checks, helper components to “eat off” those null cases, and the prop drilling that necessarily comes with all that. #### Previously ❌ [@hidden] ```tsx function Component() { const camera = useObject("camera"); const items = useList("items"); // 👎 if (camera == null || items == null) { return
Still loading...
; } return ; } ``` #### Now ✅ [@hidden] Set up a Suspense boundary once: ```tsx file="App.tsx" highlight="6" import { Suspense } from "react"; function Setup() { return ( // Once }> ); } ``` Switch to use the Suspense versions of our hooks instead of the “normal” ones: ```tsx file="liveblocks.config.ts" highlight="2" export const { suspense: { RoomProvider, useStorage, /* etc. */ }, } = createRoomContext(client); ``` Then, enjoy no more null checks everywhere in your app: ```tsx file="Component.tsx" function Component() { const camera = useStorage((root) => root.camera); const items = useStorage((root) => root.items); // No more null checking! :) } ``` ## Recommended upgrade steps [#upgrade-steps] To get the most out of the new hooks, we recommend following the steps below to gradually upgrade your app to make use of the new hooks. ### Step 1: Install the latest package [@hidden] ```bash npm install @liveblocks/client@0.18 @liveblocks/react@0.18 ``` ### Step 2: Make sure you’re setting initial presence [@hidden] We now require setting an initial presence value when you connect to a room explicitly. This ensures that every user is guaranteed to always have a known presence value. Check that you have this in your config file: ```tsx file="liveblocks.config.ts" highlight="3" ``` If your app somehow doesn’t use Presence, you can just set an empty object (`{}`) here. ### Step 3: You can remove some uncertainty from user instances [@hidden] If you have expressions in your code that look like... ```tsx user.info?.avatar; // ^ user.presence?.cursor.x; // ^ ``` You can now remove these optional chainings. The fields `info` and `presence` will now always be set on `User` instances. ### Step 4: Adopt Suspense (optional) [@hidden] Now is a great moment to opt-in to Suspense (see the [React docs](https://reactjs.org/docs/react-api.html#suspense)) with Liveblocks, if you can or want to use it in your app. We recommend it for most apps because it makes working with the new hooks even nicer. To avoid repeating ourselves, please follow the instruction below. Follow [these instructions to adopt Suspense](/docs/api-reference/liveblocks-react#suspense). Don’t worry, we’ll wait. Now that you have updated your app to Suspense, you should be able to remove all these pesky `null` checks from your code. ```tsx highlight="6-8" function Component() { const a = useMap("a"); const b = useList("b"); const c = useObject("c"); if (a == null || b == null || c == null) { return ; } /* ... */ } ``` Afterward, please verify that your app still works like normal. ### Step 5a: Replace reads with `useStorage` [@hidden] We recommend rewriting all usages of `useList`, `useObject` and `useMap` if those are used for _reading_ data only. If used only for reading values from Storage, you could turn these into an equivalent `useStorage` call, which has fewer gotchas. For example, change: ```tsx // ❌ const obj = useObject("a"); const list = useObject("b"); const map = useMap("c"); ``` to: ```tsx // ✅ const obj = useStorage((root) => root.a); const list = useStorage((root) => root.b); const map = useStorage((root) => root.c); ``` Note that the `root` argument you receive here is the immutable normal JavaScript equivalent of your entire Storage tree, as returned by calling [`.toImmutable`][]. So this means that if you have been manually converting the mutable Live structures to normal data structures, you no longer have to do this: ```tsx // ❌ obj.toObject(); list.toArray(); ``` ```tsx // ✅ obj; // Already a normal JS object list; // Already a normal JS array ``` Please note that `useList`, `useObject`, or `useMap` are not deprecated and still work with the same behavior as before. We just no longer recommend their use. ### Step 5b: Replace mutations with useMutation [@hidden] If you are (also) using `useList`, `useObject` or `useMap` to obtain a mutable reference to the Live structure to _mutate_ it, you can rewrite those use cases to use the new `useMutation` hook instead. For example: ```tsx // ❌ const obj = useObject("a"); return ( { obj.set("name", e.currentTarget.value); // ^^^ Live object used for mutating }} /> ); ``` The idiomatic way to deal with Storage is to _consume_ data using simple/normal JS data structures and to _mutate_ data using a callback function that you can create with `useMutation`, which provides access to the mutable Live structures. ```tsx // ✅ const name = useStorage((root) => root.a.name); const setName = useMutation(({ storage }, newName) => { storage.get("a").set("name", newName); // ^^^ Mutation goes here }, []); return ( setName(e.currentTarget.value)} /> ); ``` Even though in this contrived example it may look more complicated, in large apps this pattern will vastly simplify your app’s complexity. See the [`useMutation`][] documentation to learn all the details, because it has a lot more in store. ### Step 6: Get rid of `room.subscribe()` calls [@hidden] Historically the only way to get full control over exactly when and how your components would rerender was to use the low-level `room.subscribe()` API. Most, if not all, of these use cases can be replaced by an equivalent, yet much simpler call to `useStorage` with a selector function that does an equivalent thing. **Common use case: subscribing to nested data** If you are using `room.subscribe` to manually rerender components when nested data changes, you can replace it by “just” selecting the nested fields you’re interested in. See [this example](#subscriptions). ```tsx // ✅ Automatically rerenders if pets changes (but not more often) const nested = useStorage((root) => root.scientist.pets); ``` **Common use case: subscribing to a computed value** If you are using `room.subscribe` to synchronize a computation based on multiple storage values, you can replace it by “just” doing the computation in See [this example](#multi-subscriptions). ```tsx // ✅ Automatically rerenders if computed value changes const sum = useStorage((root) => root.a + root.b); ``` If you have another use case for `room.subscribe` that you think isn’t possible to express in an equivalent `useStorage` call, please [let us know about it](https://github.com/liveblocks/liveblocks/discussions/504). We’re happy to help! ### Step 7: Get rid of manual batch calls [@hidden] Most, if not all, cases of manually calling `useBatch` or `room.batch` should no longer be needed and can be replaced by `useMutation`, which automatically batches already! That’s it! If you run into issues with these new patterns and you need help, please [let us know](https://github.com/liveblocks/liveblocks/discussions/504). We’re here to help! [`usemutation`]: /docs/api-reference/liveblocks-react#useMutation [`.toimmutable`]: /docs/api-reference/liveblocks-client#LiveObject.toImmutable --- meta: title: "Upgrading to 0.19" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 0.19" --- In the Liveblocks 0.19 release, we’re adding support for Zustand v4 to `@liveblocks/zustand`. Zustand v4 brings greatly improved TypeScript types to its APIs, enabling us to enhance the quality of our types, bringing it in line with our React package. Let’s take a look! ## Upgrading steps by package ### @liveblocks/react [#react] To update `@liveblocks/react` to 0.19, run the following command using your preferred package manager: ```bash npm install @liveblocks/client@0.19 @liveblocks/react@0.19 ``` #### useOther now requires a selector argument [@hidden] `useOther` now requires a [selector](/docs/api-reference/liveblocks-react#selectors-receive-immutable-data) function argument. You will need to replace instances of `useOther` that do not use a selector in your codebase. #### Previously ❌ [@hidden] ```tsx const other = useOther(id); // 👈 does not include a selector ``` #### Now ✅ [@hidden] ```tsx const other = useOther(id, (other) => other); // 👈 requires a selector ``` #### Include unstable_batchedUpdates if you use React 17 [@hidden] We’ve added support to prevent the stale props/zombie child scenario. To avoid this issue, we enforce passing the `unstable_batchedUpdates` prop to `RoomProvider`. This section only applies if you are using React 17 or lower. You do not need to do anything if you are using React 18 or higher. ```tsx highlight="2,8" // ⚠️ Only if you’re using React 17 or lower import { unstable_batchedUpdates } from "react-dom"; // 👈 ``` For additional context, see the [troubleshooting guide](/docs/platform/troubleshooting#stale-props-zombie-child) ### @liveblocks/redux [#redux] To update `@liveblocks/redux` to 0.19, run the following command using your preferred package manager: ```bash npm install @liveblocks/client@0.19 @liveblocks/redux@0.19 ``` #### Update use of default export liveblocksEnhancer [@hidden] The main export has been renamed, so you will need to update your imports and use of the enhancer: #### Previously ❌ [@hidden] ```ts import { enhancer } from "@liveblocks/redux"; ``` #### Now ✅ [@hidden] ```ts import { liveblocksEnhancer } from "@liveblocks/redux"; ``` #### Remove the second argument to state.liveblocks.enterRoom [@hidden] When calling `state.liveblocks.enterRoom()`, you should not pass an explicit initial state. It will use the state in your Redux store, for consistency and ease of use. To migrate, make the following code changes: #### Previously ❌ [@hidden] ```ts useEffect(() => { enterRoom("room-id", { todos: [], // 👈 remove explicit initial state }); }); ``` #### Now ✅ [@hidden] ```ts useEffect(() => { enterRoom("room-id"); }); ``` ### @liveblocks/zustand [#zustand] In 0.19 we added support for Zustand v4 (specifically v4.1.3 or higher) and will no longer support Zustand v4.1.2 or lower. This is because Zustand v4.1.3 brings greatly improved TypeScript types to its APIs, and consequently, we can improve our internal types. To migrate, make the following code changes: To update `@liveblocks/zustand` to 0.19, run the following command using your preferred package manager: ```bash npm install @liveblocks/client@0.19 @liveblocks/zustand@0.19 ``` - Change these imports, if applicable, and rename accordingly: #### Previously ❌ [@hidden] ```ts import { middleware } from "@liveblocks/zustand"; import type { LiveblocksState } from "@liveblocks/zustand"; ``` #### Now ✅ [@hidden] ```ts import { liveblocks } from "@liveblocks/zustand"; import type { WithLiveblocks } from "@liveblocks/zustand"; ``` - Update to the Zustand v4 recommended pattern: #### Previously ❌ [@hidden] ```ts create(liveblocks(...)) ``` #### Now ✅ [@hidden] ```ts create>()(liveblocks(...)) ``` To be clear: 1. First, move the type annotation away from the `liveblocks` middleware call, and onto the `create` call. 2. Next, wrap your `MyState` type in a `WithLiveblocks<...>` wrapper. This will make sure the injected `liveblocks` property on your Zustand state will be correctly typed. 3. Finally, make sure to add the extra call `()` wrapper, needed by Zustand v4 now: ```ts create>()(liveblocks(...)) // ^^ Not a typo ``` - Remove the second argument to `state.liveblocks.enterRoom()`: it no longer takes an explicit initial state. Instead, it’s automatically be populated from your Zustand state. ## Improvements [#improvements] This release brings several changes to `@liveblocks/react`, which improve rendering performance and stability. Additionally, we have refactored our internal packages to increase code sharing. You can review the [release notes](https://github.com/liveblocks/liveblocks/releases) for more details. ### @liveblocks/react [#react-improvements] #### New shouldInitiallyConnect prop [@hidden] We added a new property `shouldInitiallyConnect` to `RoomProvider`, which lets you control whether or not the room connects to Liveblock servers. By default, it will check the `typeof window` to determine if it should connect. When using SSR, you can set it to `false` to prevent the room from connecting to Liveblocks servers. ```tsx highlight="4" ``` #### Addition of @liveblocks-core package [@hidden] We restructured our internal packages to increase code sharing. You may notice a new dependency in your dependency tree: `@liveblocks/core`. It contains private APIs that aren’t intended for direct consumption. ### @liveblocks/client [#client-improvements] #### New shouldInitiallyConnect option [@hidden] Similar to the [`shouldInitiallyConnect`](#react-shouldInitiallyConnect) prop in `@liveblocks/react`, we added a `shouldInitiallyConnect` option to `client.enter`. The default value is `true`. You may want to set it to `false` when using SSR, or if you would prefer to establish the WebSocket connection later. ```tsx client.enter("room", { initialPresence: {}, shouldInitiallyConnect: false, // 👈 for SSR using the @liveblocks/client package }); ``` ### @liveblocks/\* [#all] All packages now provide an `isReadOnly` flag on user instances. It is available by calling `getSelf()` and `getOthers`. `isReadOnly` is true when storage is read-only, as well as when a user has `read` permission to the `room` and `write` permission to `presence`. ```ts highlight="3,5" const me = room.getSelf(); me.isReadOnly; // boolean const others = room.getOthers(); for (const other of others) { other.isReadOnly; // boolean } ``` You can learn more about room permissions under [authentication](/docs/authentication). That’s it! If you run into issues with these new patterns and you need help, please let us know [by email](mailto:support@liveblocks.io) or by joining our [Discord community](/discord)! We’re here to help! --- meta: title: "Upgrading to 1.0" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 1.0" --- Liveblocks 1.0 is our first major release, marking the end of the beta period for storage and conflict-free data types. This release includes improvements to our pricing model that make billing more predictable. Additionally, we have vastly improved the ability to animate cursors at up to 60fps by updating the throttle option in `createClient`. The primary change you should note is the transition to making the `userId` mandatory in the `authorize` option. This change is in line with our [new pricing model](https://liveblocks.io/pricing). Let’s take a look! ## Upgrading steps by package ### @liveblocks/node [#node] To update `@liveblocks/node` to 1.0, run the following command using your preferred package manager: ```bash npm install @liveblocks/node@1.0.0 ``` #### Update the authorize option [@hidden] We have updated the `authorize` method to make `userId` mandatory. This change ties into our new [pricing model](https://liveblocks.io/pricing), which is based on Monthly Active Users (MAU) instead of connections. We use `userId` to track MAU associated with a Liveblocks account. ```ts highlight="20" import { authorize } from "@liveblocks/node"; // Replace this key with your secret key provided at // https://liveblocks.io/dashboard/projects/{projectId}/apikeys const secret = "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx"; 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. */ const room = req.body.room; const response = await authorize({ room, secret, // Corresponds to the UserMeta[id] type defined in liveblocks.config.ts userId: "123", // Required groupIds: ["456"], // Optional userInfo: { // Optional, corresponds to the UserMeta[info] type defined in liveblocks.config.ts name: "Ada Lovelace", color: "red", }, }); return res.status(response.status).end(response.body); } ``` If you do not use the `authorize` function because you are using the public API key method, a cookie will be set in the browser when a user connects to a room. This cookie will be used to track MAU associated with a Liveblocks account and will expire after 30 days. ### Authorize endpoint [#authorize-endpoint] If you do not use the `authorize` function, but call the `authorize` endpoint with a secret key instead, you should pass the `userId` even though it is not mandatory on the API level because the userID will be used for MAU. Set the `Authorization: Bearer` header to your secret key. ```ts POST https://api.liveblocks.io/v2/rooms/{roomId}/authorize ``` Set the following the request body: ```ts highlight="3" { "userId": "user123", "groupIds": [ "g1", "g2" ], "userInfo": { "name": "bob", "colors": [ "blue", "red" ] } } ``` ## Improvements [#improvements] We added additional features to [Webhooks](/docs/platform/webhooks), enhancements to the `Room` class, and more. You can review the [release notes](https://github.com/liveblocks/liveblocks/releases) for more details. ### @liveblocks/client [#client-improvements] To update `@liveblocks/client` to 1.0, run the following command using your preferred package manager: ```bash npm install @liveblocks/client@1.0.0 ``` #### Improvements to throttle allow animation of up to 60fps [@hidden] You can now specify the `throttle` option in [`createClient`](https://liveblocks.io/docs/api-reference/liveblocks-client#createClientThrottle) that can go as low as 16ms. If not set, the default value is 100ms. ```ts highlight="5" import { createClient } from "@liveblocks/client"; const client = createClient({ /* ... other options ... */ throttle: 16, }); ``` You can see the marked improvement in the animation from 80ms (original limit) to 16ms in this [tweet](https://twitter.com/ctnicholasdev/status/1622656511758700546). If you want to try it out for yourself, check out [live cursors example](https://liveblocks.io/examples/live-cursors/nextjs). #### New methods available on the Room class [@hidden] We added new methods to the `Room` class that you can use to obtain the storage status of a room, subscribe to storage status changes, or reconnect to a room whenever necessary. You can also use the [Liveblocks DevTools extension](/devtools) to visualize realtime changes to storage while implementing these changes. Use `room.getStorageStatus` to retrieve the storage status of a room. The statuses are: - `not-loaded`: Initial state when entering the room. - `loading`: Once the storage has been requested via room.getStorage(). - `synchronizing`: When some local updates have not been acknowledged by Liveblocks servers. - `synchronized`: Storage is in sync with Liveblocks servers. Use `room.subscribe` to subscribe to storage status changes. This method returns an unsubscribe function. ```ts room.subscribe("storage-status", (status) => { // Implement your logic here switch (status) { case "not-loaded": break; case "loading": break; case "synchronizing": break; case "synchronized": break; default: break; } }); ``` You can use `room.reconnect()` to close the room connection and try to [reconnect](/docs/api-reference/liveblocks-client#Room.reconnect). ### @liveblocks/node [#node-improvements] #### Webhooks enhancements [@hidden] We have added two new events to our Webhooks functionality: `RoomCreatedEvent` and `RoomDeletedEvent`. We have also added a `WebhookHandler` class to make it easier to verify event requests from Liveblocks’ Webhooks functionality. It also provides fully typed `WebhookEvents`. ```ts import { WebhookHandler } from "@liveblocks/node"; const webhookHandler = new WebhookHandler(process.env.SECRET); const event = webhookHandler.verifyRequest({ headers: req.headers, rawBody: req.body, }); ``` Verification of events is critical to ensure that the events are coming from Liveblocks and not from a malicious source. Check out our [Webhooks guide](https://liveblocks.io/docs/platform/webhooks) for more details. That’s it! If you have issues with these new patterns and need help, please let us know [by email](mailto:support@liveblocks.io) or by joining our [Discord community](/discord)! We’re here to help! --- meta: title: "Upgrading to 1.10" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 1.10" --- Notifications is the first Liveblocks product that works across multiple rooms, and this has required us to introduce breaking changes to ensure a consistent API for Comments and Notifications. ## How to upgrade? [#how] You can upgrade to 1.10 by downloading the latest version of each Liveblocks package you’re using, for example in a React app: ```bash npm install @liveblocks/client@latest @liveblocks/react@latest @liveblocks/react-ui@latest @liveblocks/node@latest ``` If you’re using any other Liveblocks packages make sure to update those too. ## All changes are for Comments and Notifications If you’re not using Comments or Notifications, there are no breaking changes for you! However, if you are using these, or intend to use them in future, keep reading. ## Security Users must see the notifications across multiple rooms when they open their inbox. This is the first time we must expose an API not associated with a single room. It forced us to introduce a minor breaking change in our authentication mechanism. Using [`useInboxNotifications`](/docs/api-reference/liveblocks-react#useInboxNotifications) will call your authentication endpoint without a `room`. ### createClient with callback If you use `createClient` with `authEndpoint` [as a callback](/docs/api-reference/liveblocks-client#createClientCallback), `room` is now optional. ```typescript const client = createClient({ authEndpoint: (room) => { // The `room` argument will be `undefined` if you're using `useInboxNotifications` }, }); ``` ## Comments resolver functions Notifications needs to resolve users, as well as Comments, so we’ve lifted the resolver functions from `createRoomContext` to `createClient`. Before: ```tsx file="liveblocks.config.ts" highlight="7-12" // ❌ Before - Liveblocks 1.9 const client = createClient({ authEndpoint: "/api/liveblocks-auth", }); const { RoomProvider } = createRoomContext({ async resolveUsers({ userIds, roomId }) { // ... }, async resolveMentionSuggestions({ text, roomId }) { // ... }, }); ``` After: ```tsx file="liveblocks.config.ts" highlight="4-9" // ✅ After - Liveblocks 1.10 const client = createClient({ authEndpoint: "/api/auth/liveblocks-auth", async resolveUsers({ userIds }) { // ... }, async resolveMentionSuggestions({ text }) { // ... }, }); const { RoomProvider } = createRoomContext(); ``` Because the resolvers are no longer room-based, you’ll notice that `roomId` is no longer in the parameters. ### Resolver types Because the resolvers have moved to the `client`, the `ResolveUsersArgs` and `ResolveMentionSuggestionsArgs` types are now exported from `@liveblocks/client` instead of `@liveblocks/react`. ```tsx highlight="5,11" // ❌ Before - Liveblocks 1.9 import { ResolveUsersArgs, ResolveMentionSuggestionsArgs, } from "@liveblocks/react"; // ✅ After - Liveblocks 1.10 import { ResolveUsersArgs, ResolveMentionSuggestionsArgs, } from "@liveblocks/client"; ``` Relatedly, [`stringifyCommentBody`](/docs/api-reference/liveblocks-node#stringify-comment-body)’s `CommentBodyResolveUsersArgs` no longer exists, and you should use `ResolveUsersArgs` instead. ```tsx highlight="5,11" // ❌ Before - Liveblocks 1.9 import { CommentBodyResolveUsersArgs } from "@liveblocks/node"; // ✅ After - Liveblocks 1.10 import { ResolveUsersArgs } from "@liveblocks/node"; ``` ## Comments CSS variables Some `elevation` and `tooltip` [CSS variables](/docs/api-reference/liveblocks-react-ui#CSS-variables) have been removed: - `--lb-tooltip-background` - `--lb-tooltip-foreground` - `--lb-tooltip-foreground-contrast` - `--lb-elevation-background` - `--lb-elevation-foreground` - `--lb-elevation-foreground-contrast` Because these no longer exist, we recommend using the basic `--lb-*` variables directly on `.lb-elevation` or `.lb-tooltip `. Before: ```css /* ❌ Before - Liveblocks 1.9 */ :root { --lb-tooltip-foreground: white; --lb-elevation-background: red; } ``` After: ```css /* ✅ After - Liveblocks 1.10 */ .lb-tooltip { --lb-foreground: white; } .lb-elevation { --lb-background: red; } ``` ## Comments overrides Comments overrides allows you to replace a string or node in default components, with another that you’ve specified. Some overrides have been changed. - `SELF` is now `USER_SELF`. - `UNKNOWN_USER` is now `USER_UNKNOWN`. - `COMMENT_REACTION_REMAINING` no longer exists, and you should use the `LIST_REMAINING_USERS` instead. - `COMMENT_REACTION_TOOLTIP` is now `COMMENT_REACTION_LIST`, and its arguments have been changed to `(list, emoji, count)`. Before: ```tsx highlight="4-7" // ❌ Before - Liveblocks 1.9 `${others} people`, COMMENT_REACTION_TOOLTIP: (emoji, list) => ( <> {list} reacted with {emoji} ), }} /> ``` After: ```tsx highlight="4-7" // ✅ After - Liveblocks 1.10 `${others} people`, COMMENT_REACTION_LIST: (list, emoji, count) => ( <> {list} reacted with {emoji} ), }} /> ``` When upgrading, remember that overrides can be applied both globally with ``, and per component, e.g. ``. Learn more in the [overrides API reference](/docs/api-reference/liveblocks-react-ui#Overrides). --- meta: title: "Upgrading to 1.2" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 1.2" --- There are no breaking changes in this update, however we are introducing a new authentication method. If you’re currently using `createClient()` with the `authEndpoint` option, we recommend you read on. Liveblocks 1.2 provides new ways to specify who can access certain rooms, and with which permissions they can enter. Our new-style security tokens also bring speed improvements, allowing clients to connect Liveblocks rooms even quicker, as well as permitting authorization tokens that can be used for multiple rooms. ## Public key authentication changes [#public-auth-changes] If you’re currently using Liveblocks **public keys**, no changes are required. With Liveblocks 1.2, entering rooms with your public key will be noticeably quicker. If you’re currently using Liveblocks public keys, no changes are required to your application. ## Private key authentication changes [#private-auth-changes] If you’re currently using Liveblocks **private keys** we **highly recommend** you upgrade your auth back end. Upgrading your existing auth back end will opt you in to using our new-style auth tokens, which offer the following benefits: - Grant users permission to multiple rooms in one transaction, meaning fewer requests on your back end. - Much quicker to join rooms, particularly any after the first. - Unlock access to upcoming features, such as [Comments](/comments). If you’re currently using Liveblocks private keys, no changes are strictly necessary, but we do highly recommend you upgrade your application’s back end. ## How to upgrade? [#how] You can upgrade to 1.2 by downloading the latest version of each Liveblocks package you’re using, for example in a React app: ```bash npm install @liveblocks/client@latest @liveblocks/node@latest @liveblocks/react@latest ``` If you’re using any other Liveblocks packages make sure to update those too. We’ll walk you through the necessary changes below, but first, if you currently have a Liveblocks application in production, we recommend following a rollout plan, to prevent any users running an old Liveblocks client, during the upgrade, from having issues. ### Rollout plan Keep your existing back end endpoint (e.g. `/api/auth`). Create a new authentication endpoint for the upgrade (e.g. `/api/liveblocks-auth`). In your front end, point `createClient()`’s `authEndpoint` URL to the new endpoint. Done! You can deploy your application now. Later, when all your application’s clients have been upgraded to the latest version, you can safely remove the old endpoint. ## Deciding which token to use [#deciding] In Liveblocks 1.2 there are two new ways to authenticate with `@liveblocks/node`. - [Access tokens](#access-tokens) are recommend for most applications. - [ID tokens](#id-tokens) are best if you’re using fine-grained permissions with our REST API. Access tokens and ID tokens both allow for multiple room support, but have different sources of truth. The old authentication method, single-room tokens with `authorize`, is still supported but will eventually be deprecated. ## Access tokens [#access-tokens] Access tokens are the new recommended way to authenticate, because they’re easy to manage from your custom back end. They follow the analogy of a _hotel key card_. Anyone that has a key card can enter any room that the card gives access to. It’s easy to give out these key cards right from your back end. ### Upgrading to access tokens [#upgrade-to-access-tokens] First, create a new endpoint in your back end, next to your existing `/api/auth` endpoint. We recommend going with `/api/liveblocks-auth`. Let’s implement it to issue tokens that would be equivalent to your current single-room token based setup. Create a client Create a Node.js client that allows you to interact with our REST API. ```tsx import { Liveblocks } from "@liveblocks/node"; const liveblocks = new Liveblocks({ secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx", }); ``` Start an auth session inside your endpoint Every session should have a unique user ID, which is typically the ID of the user in your database. ```tsx highlight="10-13" import { Liveblocks } from "@liveblocks/node"; const liveblocks = new Liveblocks({ secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx", }); export async function POST(request) { const user = { id: "olivier@example.com", info: { name: "Olivier" }}; const session = liveblocks.prepareSession( user.id, { userInfo: user.info } // Optional ); } ``` Give the user access to the room Give if the current user access to the room, with either `session.FULL_ACCESS`, or `session.READ_ACCESS`. ```tsx highlight="15,16" import { Liveblocks } from "@liveblocks/node"; const liveblocks = new Liveblocks({ secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx", }); export async function POST(request) { const user = { id: "olivier@example.com", info: { name: "Olivier" }}; const session = liveblocks.prepareSession( user.id, { userInfo: user.info } // Optional ); const { room } = await request.json(); session.allow(room, session.FULL_ACCESS); } ``` Authorize the user and return the result Give if the current user access to the room, with either `session.FULL_ACCESS`, or `session.READ_ACCESS`. ```tsx highlight="18,19" import { Liveblocks } from "@liveblocks/node"; const liveblocks = new Liveblocks({ secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx", }); export async function POST(request) { const user = { id: "olivier@example.com", info: { name: "Olivier" }}; const session = liveblocks.prepareSession( user.id, { userInfo: user.info } // Optional ); const { room } = await request.json(); session.allow(room, session.FULL_ACCESS); const { status, body } = await session.authorize(); return new Response(body, { status }); } ``` Point to the new endpoint In the front end of your app, update your `liveblocks.config.ts` file to connect to the new endpoint. ```ts file="liveblocks.config.ts" highlight="4" import { createClient } from "@liveblocks/client"; const client = createClient({ authEndpoint: "/api/liveblocks-auth", }); ``` You’re migrated! You’ve successfully migrated, and now have a similar authentication set up as before! However, there are other new features we can take advantage of. Bonus: Issue access to multiple rooms With Liveblocks 1.2, you can also issue access to multiple rooms, or even use a prefix-based wildcard in the room name, enabling any amount of rooms! You can learn more about this in our [access token](/docs/authentication/access-token#permissions) guide. ```tsx highlight="15-18" import { Liveblocks } from "@liveblocks/node"; const liveblocks = new Liveblocks({ secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx", }); export async function POST(request) { const user = { id: "olivier@example.com", info: { name: "Olivier" }}; const session = liveblocks.prepareSession( user.id, { userInfo: user.info } // Optional ); const { room } = await request.json(); session.allow(room, session.FULL_ACCESS); session.allow("my-room-*", session.READ_ACCESS); session.allow("my-other-room", session.READ_ACCESS); const { status, body } = await session.authorize(); return new Response(body, { status }); } ``` ### Learn more about access tokens You can find guides for your specific framework and learn more about permissions in our [access tokens authentication guides](/docs/authentication/access-token). Here’s a full working example of access tokens in a Next.js endpoint. ```ts file="app/api/liveblocks-auth/route.ts" isCollapsed isCollapsable import { Liveblocks } from "@liveblocks/node"; // Create a client const liveblocks = new Liveblocks({ secret: "{{SECRET_KEY}}", }); export async function POST(request: Request) { const user = __getUserFromDB__(request); // Start an auth session inside your endpoint const session = liveblocks.prepareSession( user.id, { userInfo: user.metadata } // Optional ); // Give the user access to the room const { room } = await request.json(); if (room && __shouldUserHaveAccess__(user, room)) { // e.g. session.allow("my-room", ["room:write"]) session.allow(room, session.FULL_ACCESS); } // Authorize the user and return the result const { status, body } = await session.authorize(); return new Response(body, { status }); } ``` ## ID tokens [#id-tokens] Are you already using our REST API to assign fine-grained permissions to each room, via [Create room](/docs/api-reference/rest-api-endpoints#post-rooms) or [Update room](/docs/api-reference/rest-api-endpoints#post-rooms-roomId) APIs? If so, ID tokens may work best for you. ID tokens follow the analogy of a _membership card_. Anyone with that membership card can try to enter a room, but your permissions will be checked at the door. This approach to permissions is most powerful because it can be set up very finely, but it comes at the cost of having to keep those permissions programmatically in sync with Liveblocks. We recommend it for advanced use cases only. ### Upgrading to ID tokens [#upgrade-to-id-tokens] If you’ve already set up room permissions using our REST API, then this should be easy! First, let’s create a new endpoint in your back end, next to your existing `/api/auth` endpoint. We recommend going with `/api/liveblocks-auth`. All you have to do now is implement it as follows: Create a client Create a Node.js client that allows you to interact with our REST API. ```tsx import { Liveblocks } from "@liveblocks/node"; const liveblocks = new Liveblocks({ secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx", }); ``` Identify the user and return the result Whichever `userId` (or `groupIds`) you pass will be used to check the permissions you configured in your Liveblocks account already. ```tsx highlight="10-15" import { Liveblocks } from "@liveblocks/node"; const liveblocks = new Liveblocks({ secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx", }); export async function POST(request) { const user = { id: "olivier@example.com", info: { name: "Olivier" }}; const { status, body } = await liveblocks.identifyUser({ userId: user.id, groupIds, // Optional }); return new Response(body, { status }); } ``` Point to the new endpoint In the front end of your app, update your `liveblocks.config.ts` file to connect to the new endpoint. ```ts file="liveblocks.config.ts" highlight="4" import { createClient } from "@liveblocks/client"; const client = createClient({ authEndpoint: "/api/liveblocks-auth", }); ``` You’re migrated! You’ve successfully migrated to id tokens! Bonus: Use our REST API to handle permissions ID tokens use permissions set with our [REST API](/docs/api-reference/rest-api-endpoints). For example, this is how you [create a room](/docs/api-reference/rest-api-endpoints#post-rooms), and give a user full access. ```ts highlight="6-8" fetch("https://api.liveblocks.io/v2/rooms", { method: "POST", body: JSON.stringify({ id: "my-room-name", defaultAccesses: [], usersAccesses: { "olivier@example.com": ["room:write"] } }), }); ``` ### Learn more about ID tokens You can find guides for your specific framework and learn more about permissions in our [ID tokens authentication guides](/docs/authentication/id-token). Here’s a full working example of ID tokens in a Next.js endpoint. ```ts file="app/api/liveblocks-auth/route.ts" isCollapsed isCollapsable import { Liveblocks } from "@liveblocks/node"; // Create a client const liveblocks = new Liveblocks({ secret: "{{SECRET_KEY}}", }); export async function POST(request: Request) { const user = __getUserFromDB__(request); // Identify the user and return the result const { status, body } = await liveblocks.identifyUser({ userId: user.id, groupIds, // Optional }); return new Response(body, { status }); } ``` If you have issues with these new patterns and need help, please let us know [by email](mailto:support@liveblocks.io) or by joining our [Discord community](/discord)! We’re here to help! --- meta: title: "Upgrading to 1.5" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 1.5" --- There’s a small breaking changes in this update, as we’re introducing a better API to enter and leave rooms. If you’re calling `client.enter()` or `client.leave()` in your application, or using our Zustand or Redux packages, we recommend you read on. ## How to upgrade? [#how] You can upgrade to 1.5 by downloading the latest version of each Liveblocks package you’re using, for example in a React app: ```bash npm install @liveblocks/client@latest @liveblocks/node@latest @liveblocks/react@latest ``` If you’re using any other Liveblocks packages make sure to update those too. ## Recommended: new enter/leave API Until now, the API to manually enter a Room using the client looked like the following: ```tsx // ❌ We recommend you stop using this API const room = client.enter("my-room", options); // Then later, when unmounting client.leave("my-room"); ``` These APIs will remain supported and unchanged, but starting with Liveblocks 1.5, there is a new preferred API, which we recommend you switch to: ```tsx // ✅ Prefer this API instead const { room, leave } = client.enterRoom("my-room", options); // Then later, when unmounting leave(); ``` We’ve changed this API to return a new “leave” function every time a room reference is requested. This allows sharing the same room connection with two or more parts of your application, without those parts competing for control of the room connection. The room connection will only be terminated after every `leave` function has been called. This enables more advanced use cases such as supporting multiple `RoomProvider` instances for the same room ID, in different parts of your application, or using our React package for one part of your application, while using a Zustand store for another. ## Upgrading for Zustand users If you’re using our Zustand package, there’s a breaking change—you no longer need to pass the room ID to leave the room: ```tsx const { liveblocks: { leaveRoom }, } = useStore(); // ❌ Before leaveRoom("my-room-name"); // ✅ After leaveRoom(); ``` ## Upgrading for Redux users If you’re using our Redux package, there’s a similar breaking change—you no longer need to pass the room ID to leave the room: ```tsx const dispatch = useDispatch(); // ❌ Before dispatch(actions.leaveRoom("my-room-name")); // ✅ After dispatch(actions.leaveRoom()); ``` --- meta: title: "Upgrading to 1.9" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 1.9" --- There’s a tiny breaking change related to Comments in this update. Each date returned is now a `Date` object, whereas previously each was a `string`. ## How to upgrade? [#how] You can upgrade to 1.9 by downloading the latest version of each Liveblocks package you’re using, for example in a React app: ```bash npm install @liveblocks/client@latest @liveblocks/react@latest @liveblocks/react-comments@latest @liveblocks/node@latest ``` If you’re using any other Liveblocks packages make sure to update those too. ## Dates are no longer strings Each date returned from Comments is now a `Date` object, whereas previously each was a `string`. An example of this is `createdAt`, a value attached to each thread. Before: ```tsx // ❌ Before - Liveblocks 1.8 const { threads } = useThreads(); // "2023-12-15T14:15:22Z" console.log(threads[0].createdAt); ``` After: ```tsx // ✅ After - Liveblocks 1.9 const { threads } = useThreads(); // Date console.log(threads[0].createdAt); ``` Every Comments-related date has been updated, such as `createdAt`, `updatedAt` properties in threads, and `editedAt`, `deletedAt` in comments, so take care to check every date instance you’re using. It’s likely that no changes are required after this update. --- meta: title: "Upgrading to 2.0" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 2.0" --- Liveblocks 2.0 is our second major release, and focuses on removing any rough edges and smoothening the developer experience. It makes Liveblocks simpler to set up for new users as well as for existing users. Our long term goal is to empower you to write and evolve your apps in the best way possible, and at enterprise scale. Skip ahead to the [how to upgrade](#how-to-upgrade) section. ## Rationale Liveblocks has always embraced the benefits of static typing with TypeScript. Roughly two years ago, we published 0.17, which [introduced](/blog/whats-new-in-v0-17) the pattern of calling `createRoomContext()` and returning a “bundle” of type-safe hooks that you could then use in your application. This pattern worked nicely because all the hooks were bound to your custom type definitions for `Presence`, `Storage`, etc. in one single place, and every Liveblocks API would deeply know about it. No need for manual type annotations anywhere else in your codebase. ### Downsides to this approach There were, however, a couple of downsides to this approach that have always kept itching: - Importing hooks from a local config file was a little bit awkward. It wasn’t an all too familiar pattern for most users. - Re-exporting the list of hooks took maintenance. If we added a new hook, you’d have to also re-export it manually. - Due to how TypeScript works, the type params you’d provide to `createRoomContext()` had a fixed ordering to them. If you wanted to specify only `Presence` and `RoomEvent`, you’d have to “skip” some type params, by doing `createRoomContext()` or similar. ### Introducing Liveblocks 2.0 With 2.0, this all becomes a lot simpler. We’ve done a lot of internal refactoring to make this possible. We’ll dive in soon, but here is a sneak peek: ```tsx showLineNumbers={false} file="liveblocks.config.ts" // ❌ This pattern is no longer recommended unless you need multiple room types export const { suspense: { RoomProvider, useRoom, useThread }, } = createRoomContext< MyPresence, MyStorage, MyUserMeta, MyRoomEvent, MyThreadMetadata >(client); // ✅ After declare global { // These custom types are all optional, just define the ones you want/need interface Liveblocks { Presence: MyPresence; Storage: MyStorage; UserMeta: MyUserMeta; RoomEvent: MyRoomEvent; ThreadMetadata: MyThreadMetadata; } } ``` ```tsx showLineNumbers={false} file="Component.tsx" // ❌ Before import { RoomProvider, useRoom, useThreads } from "./liveblocks.config.ts"; // ✅ After import { RoomProvider, useRoom, useThreads } from "@liveblocks/react/suspense"; // or import { RoomProvider, useRoom, useThreads } from "@liveblocks/react"; ``` We’ll first go over all the breaking changes, then we’ll show you how to simplify your codebase. ## How to upgrade [#how-to-upgrade] First of all, let’s upgrade all Liveblocks dependencies to their latest versions. The easiest way to do that is to run the following command: ```bash npx liveblocks@latest upgrade ``` There are also some **breaking changes** in this update. **Most users will not run into any of these**, but there is a chance that some of these will affect your situation. Making the necessary code adjustments should however be easy. In many cases, we provide a codemod that makes the actual change for you, so you don’t have to do so manually. ### Breaking change 1: renamed package [#bc1] This breaking change only affects users of `@liveblocks/react-comments`. We’ve renamed our package `@liveblocks/react-comments` to `@liveblocks/react-ui`, because our library of pre-built UI components now contains more than just Comments-related components. Please adjust your imports. Run the following **codemod** or manually make the changes: ```bash npx @liveblocks/codemod@latest react-comments-to-react-ui ``` This will change your imports like this: ```tsx showLineNumbers={false} // ❌ Before import { Thread } from "@liveblocks/react-comments"; // ✅ After import { Thread } from "@liveblocks/react-ui"; ``` And also: ```tsx showLineNumbers={false} // ❌ Before // ✅ After ``` ### Breaking change 2: renamed exports in our Node package [#bc2] This breaking change only affects users of `@liveblocks/node`. To avoid confusion with the newly introduced custom [`RoomInfo`](#New-custom-type-RoomInfo) type, we’ve renamed the `RoomInfo` type in our `@liveblocks/node` package. Run the following **codemod** or manually make the changes: ```bash npx @liveblocks/codemod@latest room-info-to-room-data ``` This will change: ```tsx showLineNumbers={false} // ❌ Before import { RoomInfo } from "@liveblocks/node"; const rooms: RoomInfo[] = []; // ✅ After import { RoomData } from "@liveblocks/node"; const rooms: RoomData[] = []; ``` ### Breaking change 3: client methods from our Node package no longer take type params [#bc3] This breaking change only affects users of `@liveblocks/node`. In `@liveblocks/node`, none of the client methods, like `liveblocks.getThread()`, take type params any longer. Make the following changes: ```tsx showLineNumbers={false} // ❌ Before await liveblocks.createThread(); await liveblocks.editThreadMetadata(); await liveblocks.getThreads(); await liveblocks.getThread(); // ^^^^^^^^^^^^^^^^ No longer possible // ✅ After await liveblocks.createThread(); await liveblocks.editThreadMetadata(); await liveblocks.getThreads(); await liveblocks.getThread(); // In liveblocks.config.ts declare global { interface Liveblocks { ThreadMetadata: MyThreadMetadata; } } ``` You should use global type augmentation instead. Please see [“Simplifying your Liveblocks application”](#simplifying) below for a lot more detail on this transition. ### Breaking change 4: changed default export to named export in Yjs package [#bc4] This breaking change only affects users of `@liveblocks/yjs`. To make discoverability and refactorings easier, and to avoid confusion with the newly introduced [`LiveblocksProvider`][] in our React package, we’re no longer using default exports, but named exports only. Run the following **codemod** or manually make the changes: ```bash npx @liveblocks/codemod@latest remove-yjs-default-export ``` This will change your import (and its usage) like this: ```tsx showLineNumbers={false} // ❌ Before import LiveblocksProvider from "@liveblocks/yjs"; const yDoc = new Y.Doc(); const yProvider = new LiveblocksProvider(room, yDoc); ``` ```tsx showLineNumbers={false} // ✅ After import { LiveblocksYjsProvider } from "@liveblocks/yjs"; const yDoc = new Y.Doc(); const yProvider = new LiveblocksYjsProvider(room, yDoc); ``` ### Breaking change 5: minor LiveList constructor change [#bc5] This breaking change only affects users of Storage. The `LiveList()` constructor’s argument is no longer optional, because it causes unneeded but confusing type inference issues. Run the following **codemod** or manually make the changes: ```bash npx @liveblocks/codemod@latest live-list-constructor ``` This will add an array to empty `LiveList` constructors: ```tsx showLineNumbers={false} // ❌ Before const mylist = new LiveList(); // ✅ After const mylist = new LiveList([]); ``` ### Breaking change 6: new webhook event types [#bc6] This breaking change only affects users of webhooks. The webhook event `NotificationEvent`’s type can represent multiple kinds of notifications: `"thread"`, `"textMention"`, and custom ones (e.g. `"$myNotification"`). If you were using properties only available on the `"thread"` kind (e.g. `threadId`), you will need to first check for the kind of notification before accessing them. ```ts showLineNumbers={false} // ❌ Before const threadId = event.data.threadId; // ✅ After if (event.data.kind === "thread") { const threadId = event.data.threadId; } ``` ### Breaking change 7: removed deprecated APIs [#bc7] All of the following APIs have been removed in 2.0, as they were deprecated multiple versions ago. **Affecting `@liveblocks/client`:** - `Client.enter()` has been replaced by `Client.enterRoom()` - `Client.leave()` has been replaced by `Client.enterRoom()`, which returns a `leave` function - Client option `fetchPolyfill`, `WebSocketPolyfill`, are replaced by `polyfills: { fetch, WebSocket }` - Legacy option `shouldInitiallyConnect` is now renamed to `autoConnect` - Legacy connection status APIs, e.g. `room.getConnectionState()` and `.subscribe("connection")`. You can use `.getStatus()` or `.subscribe("status")` instead. - `user.isReadOnly` field is replaced by `!user.canWrite` (note the negation here) - The `Others` type. Please change to `readonly User[]`. **Affecting `@liveblocks/react`:** - The `useMap`, `useList`, and `useObject` hooks. These have been deprecated since the release of 0.18 (more than two years ago). Please see [the 0.18 upgrade guide](/docs/platform/upgrading/0.18#Step-5a:-Replace-reads-with-useStorage) for tips on how to rewrite these hooks to `useStorage`. - The second argument `options` to `createRoomContext(client, options)` has been removed. These options have been moved to the client. **Affecting `@liveblocks/node`:** - Remove legacy `authorize` method from `@liveblocks/node`. Please refer to [the 1.2 upgrade guide](/docs/platform/upgrading/1.2#how) to learn how to upgrade your auth endpoint. **Affecting `@liveblocks/redux` and `@liveblocks/zustand`:** - Legacy aliased exports in Zustand/Redux packages are removed ## Simplifying your Liveblocks application (optional) [#simplifying] This section is optional, but highly recommended. You can do it now, or come back to do it later. If you have dealt with the breaking changes above, or concluded they they don’t apply to your situation, we can go a step further and really simplify your Liveblocks setup. If your Liveblocks application has multiple room types, and therefore you have multiple Liveblocks config files—one for each room type—then you cannot switch to a single global type definition. In that advanced case, the following simplifications do not apply to you. We will keep supporting the `createRoomContext` API for these advanced use cases, but no longer recommend it for the more common case of having just a single room type. ### Step 1: Use the new global Liveblocks custom types [#step1] Go to your `liveblocks.config.ts`, find your `createRoomContext()` call. Now decide which codemod to run. **Option 1: You are using Suspense hooks.** (`const { suspense: { useRoom, ... } }`) ```bash npx @liveblocks/codemod@latest remove-liveblocks-config-contexts --suspense ``` **Option 2: You are using classic hooks.** (`const { useRoom, ... }`) ```bash npx @liveblocks/codemod@latest remove-liveblocks-config-contexts ``` Running either of these will make the following changes. ```tsx showLineNumbers={false} // ❌ Before export const { ... } = createRoomContext(client); ``` ```tsx showLineNumbers={false} // ✅ After declare global { interface Liveblocks { Presence: MyPresence; Storage: MyStorage; UserMeta: MyUserMeta; RoomEvent: MyRoomEvent; ThreadMetadata: MyThreadMetadata; } } ``` Secondly, it will change all imports in your code base to import the hooks directly instead: ```tsx showLineNumbers={false} // ❌ Before import { RoomProvider, useRoom, ... } from "./liveblocks.config.ts"; ``` ```tsx showLineNumbers={false} // ✅ After import { RoomProvider, useRoom, ... } from "@liveblocks/react/suspense"; // Option 1 import { RoomProvider, useRoom, ... } from "@liveblocks/react"; // Option 2 ``` ### Step 2: Set up a LiveblocksProvider [#step2] At this point, there should not be any new TypeScript issues. However, running the code will not yet work. This is because previously the hooks were bound to the `client` instance by passing it to the `createRoomContext()` factory, which we now removed. When using the global types, we’ll have to provide the Liveblocks client otherwise. The way to do it is to use a pretty standard React provider. Make the following change: ```tsx showLineNumbers={false} file="liveblocks.config.ts" // ❌ Before, you no longer have to use createClient() import { createClient } from "@liveblocks/client"; const client = createClient({ authEndpoint: "/api/liveblocks-auth", throttle: 16, /* etc */ ); ``` ```tsx showLineNumbers={false} file="layout.tsx" // ✅ After "use client"; import { LiveblocksProvider } from "@liveblocks/react"; export function Layout({ children }) { return ( // Move options here, client will be created for you {children} ); } ``` We recommend placing the `LiveblocksProvider` as high up your component tree as you can, preferably in your `_app.tsx` or `layout.tsx` (or equivalent for your React framework of choice). Unfortunately, we cannot provide a codemod for this step, because we cannot mechanically decide where exactly to inject the Liveblocks provider in your application. If you were exporting the `client` instance before and have components that directly accessed it before, you can now obtain a reference to the `client` instance that the `LiveblocksProvider` creates for you using the `useClient` hook: ```tsx showLineNumbers={false} // ❌ Before import { client } from "./liveblocks.config"; function MyComponent() { doSomethingWith(client); } ``` ```tsx showLineNumbers={false} // ✅ After import { useClient } from "@liveblocks/react"; // or import { useClient } from "@liveblocks/react/suspense"; function MyComponent() { const client = useClient(); doSomethingWith(client); } ``` ### Step 3: Optional cleanup of type params [#step3] If you also exported your `Presence`, `Storage`, etc types from `liveblocks.config.ts` before, you no longer have to. The main reason to export these before was to use them in helper functions that used some of the Liveblocks types, like `User`, or `Room`. ```bash npx @liveblocks/codemod@latest remove-unneeded-type-params ``` For example: ```tsx showLineNumbers={false} import type { Room, User } from "@liveblocks/client"; import type { MyPresence, MyStorage } from "./liveblocks.config"; // ❌ Before function isAdult(user: User) { // ^^^^^^^^^^^^^^^^^^^^^^ return user.info.age >= 18; // ^^^ Coming from MyUserMeta } // ❌ Before function doSomethingWithRoom( room: Room // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) { /* ... */ } ``` This is no longer needed. You can simply remove them. TypeScript will still know about your custom `age` property on `user.info`. ```tsx showLineNumbers={false} import type { Room, User } from "@liveblocks/client"; // ✅ After function isAdult(user: User) { return user.info.age >= 18; // ^^^ Still coming from configured UserMeta custom type } // ✅ After function doSomethingWithRoom(room: Room) { /* ... */ } ``` ## Improvements Furthermore, the following miscellaneous quality-of-life improvements have been made that are non-breaking changes. ### `ClientSideSuspense` no longer needs a function Previously, the `ClientSideSuspense` helper needed a function as its `children` prop, but it no longer has to. ```bash npx @liveblocks/codemod@latest simplify-client-side-suspense-children ``` This will change: ```tsx showLineNumbers={false} // ❌ Before }> {() => } ``` ```tsx showLineNumbers={false} // ✅ After }> ``` ### Improved `InboxNotification` props types When passing custom components to the `kinds` prop of [`InboxNotification`][], you could use types like `InboxNotificationThreadProps` for the props. But this wasn’t always true for all notification kinds, so now you can use types named `InboxNotificationThreadKindProps` for your components, while `InboxNotificationThreadProps` describes the props of our own `InboxNotification.Thread`. ```tsx showLineNumbers={false} // ❌ Before function MyThreadNotification(props: InboxNotificationThreadProps) { return ; } ; ``` ```tsx showLineNumbers={false} // ✅ After function MyThreadNotification(props: InboxNotificationThreadKindProps) { // ^^^^ return ; } ; ``` ### New custom type `RoomInfo` By using the [`resolveRoomsInfo`][] callback from [`createClient`][] or the new [`LiveblocksProvider`][], you can attach arbitrary room data to a room, which you can retrieve with the [`useRoomInfo`][] hook. Both of these APIs will now respect the type you provide via: ```tsx showLineNumbers={false} declare global { interface Liveblocks { RoomInfo: { /* your custom type definition here */ }; } } ``` ### New custom type `ActivitiesData` By providing a custom `ActivitiesData` type, you can improve how your custom notifications and their activities’ data are typed. ```tsx showLineNumbers={false} declare global { interface Liveblocks { // Custom activities data for custom notification kinds ActivitiesData: { // Example, a custom $alert kind $alert: { title: string; message: string; }; }; // Other kinds // ... } } ``` That’s it! ## Questions? Please ask! If you have any trouble with these new patterns, run into a bug with one of the codemods, or otherwise need help, please let us know [by email](mailto:support@liveblocks.io) or by joining our [Discord community](/discord)! We’re here to help! [`createClient`]: /docs/api-reference/liveblocks-client#createClient [`LiveblocksProvider`]: /docs/api-reference/liveblocks-react#LiveblocksProvider [`InboxNotification`]: /docs/api-reference/liveblocks-react-ui#InboxNotification [`resolveRoomsInfo`]: /docs/api-reference/liveblocks-client#createClientResolveRoomsInfo [`useRoomInfo`]: /docs/api-reference/liveblocks-react#useRoomInfo --- meta: title: "Upgrading to 2.15" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 2.15" --- This version of Liveblocks drops support for React versions below 18. ## Does this affect you? [#does-this-affect-you] If you are **already on React 18 or above**, you do not have to do anything. Just enjoy the smaller bundle size and performance improvements! If **you are on React 16 or 17**, you should upgrade React before upgrading Liveblocks to 2.15. See the [instructions below](#how-to-upgrade). If **you cannot upgrade to React 18 yet** for whatever reason, you can still keep using Liveblocks 2.14 which is the last version to support React 17 and below. ## How to upgrade? [#how-to-upgrade] To upgrade Liveblocks to 2.15, first upgrade React to 18 by following the [How to Upgrade to React 18](https://react.dev/blog/2022/03/08/react-18-upgrade-guide). Then, upgrade all Liveblocks packages by running: ```bash npx liveblocks@latest upgrade ``` You can now safely remove this line from your codebase: ```diff -import { unstable_batchedUpdates } from "react-dom"; // ^^^^^^^^^^^ ...or "react-native"! ``` That’s it! --- meta: title: "Upgrading to 2.16" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 2.16" --- We’ve made React changes, affecting how our error listener hook works, and how undefined metadata is filtered in Comments. ## How to upgrade Upgrade to 2.16 by downloading the latest version of each Liveblocks package you’re using. The easiest way to do this is to run the following command: ```bash npx liveblocks@latest upgrade ``` ## Does this affect you? [#does-this-affect-you] **If you’re using the [`useErrorListener`][] hook**, please read about [a behavior change](#change-1). **If you’re using Comments in React**, please read about [the change to metadata filtering](#change-2). Otherwise, no changes will affect you. ## New errors can appear in `useErrorListener` [#change-1] From 2.16, [`useErrorListener`][] will notify you about new errors. Previously, only room connection errors were previously reported, possible if you were using Presence, Storage, or Yjs. After upgrading, you can also receive a number of errors from Comments and Notifications. ```tsx import { useErrorListener } from "@liveblocks/react"; useErrorListener((error) => { switch (error.context.type) { // +++ case "CREATE_THREAD_ERROR": const { roomId, threadId, commentId, body, metadata } = error.context; break; // +++ // +++ case "MARK_INBOX_NOTIFICATION_AS_READ_ERROR": const { inboxNotificationId, roomId } = error.context; break; // +++ // Many other new errors // ... } }); ``` For a full list of possible errors, see the [`useErrorListener`][] documentation. We have also decoupled [`useErrorListener`][] from the current room. Previously it required to be nested under a [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider). Now, you can use it anywhere under [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), and it will notify you about errors from all rooms. ### To upgrade [#upgrade-change-1] No changes are necessary to upgrade, however **if you would like to keep the old behavior** and only show room connection errors, you can filter out all new errors with an early return: ```ts import { useErrorListener, useRoom } from "@liveblocks/react"; function App() { // +++ const room = useRoom(); // +++ useErrorListener((error) => { // +++ if ( error.context.roomId !== room.id && error.context.type !== "ROOM_CONNECTION_ERROR" ) { return; } // +++ // Your previous logic }); } ``` ## Ability to filter threads by absence of metadata [#change-2] From 2.16, the [`useThreads`][] (and `useUserThreads_experimental`) hooks supports filtering by _absence_ of a metadata field using `null`. ```ts // ✅ Will now return "important" threads without a `color` field const threads = useThreads({ query: { metadata: { // +++ color: null, // +++ label: "important", }, }, }); ``` Previously this was not supported, but due to a bug, using an explicit `undefined` would already allow you to do this. However, threads weren’t filtered correctly in our back end causing over-fetches. ### To upgrade [#upgrade-change-2] If you relied on this bug, change `undefined` to `null` in any metadata properties. **This will keep behavior the same**—if you don’t make this change, then your previous filtering will no longer work. ```ts title="Before" const threads = useThreads({ query: { metadata: { // ❌ Before - Will have no effect in 2.16 // +++ color: undefined, // +++ label: "important", }, }, }); ``` ```ts title="After" const threads = useThreads({ query: { metadata: { // ✅ After - Will now find threads without a `color` // +++ color: null, // +++ label: "important", }, }, }); ``` That’s it for 2.16! [`useerrorlistener`]: /docs/api-reference/liveblocks-react#useErrorListener [`usethreads`]: /docs/api-reference/liveblocks-react#useThreads --- meta: title: "Upgrading to 2.2" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 2.2" --- You must upgrade to 2.0 first, please follow the [2.0 upgrade guide](/docs/platform/upgrading/2.0). We are making `resolved` a first-class citizen property on [threads](/docs/ready-made-features/comments/concepts#Threads). You don’t have to use a thread’s metadata to set a thread as resolved anymore. ## How to upgrade? [#how] You can upgrade to 2.2 by downloading the latest version of each Liveblocks package you’re using. The easiest way to do that is to run the following command: ```bash npx liveblocks@latest upgrade ``` ## All changes are for Comments If you are not using Comments or are not using the `resolved` metadata property, there are no breaking changes for you! However, if you are and have logic based on the webhook event `threadMetadataUpdated`, keep reading. ### Webhook events When you upgrade to 2.2, the default [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component will update the thread’s first-class citizen property instead of the metadata. When a thread is marked as resolved or unresolved, we will send the events [`threadMarkedAsResolved`](/docs/platform/webhooks#ThreadMarkedAsResolvedEvent) and [`threadMarkedAsUnresolved`](/docs/platform/webhooks#ThreadMarkedAsUnresolvedEvent) instead of [`threadMetadataUpdated`](/docs/platform/webhooks#ThreadMarkedAsUnresolvedEvent). Before upgrading to 2.2, you should update your webhook endpoint to process those new events accordingly. ### REST API endpoints If you use the REST endpoint [`Edit thread metadata`](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-metadata) to update the metadata `resolved`, you should instead use the endpoints [`Mark thread as resolved`](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-resolved) and [`Mark thread as unresolved`](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-unresolved). Note that updating the `resolved` metadata property automatically updates the first-class citizen one. --- meta: title: "Upgrading to 2.24" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 2.24" --- We’ve renamed some of the concepts around notifications and notification settings to improve clarity. ## How to upgrade Upgrade to 2.24 by downloading the latest version of each Liveblocks package you’re using. The easiest way to do this is to run the following command: ```bash npx liveblocks@latest upgrade ``` ## Does this affect you? [#does-this-affect-you] If you are using notification settings in any way—with [`@liveblocks/client`][], [`@liveblocks/react`][], [`@liveblocks/node`][], or [the REST API](/docs/api-reference/rest-api-endpoints)—continue reading to see the changes. The naming changes are **backwards compatible**. Otherwise, no changes will affect you. ## Naming changes ### Rationale Historically, we used the term “notification settings” to refer to **room-level** settings that control the [inbox notifications](/docs/ready-made-features/notifications/concepts#Inbox-Notifications) received by a user (e.g. setting `"threads"` to `"all"` would make the user receive inbox notifications for any threads activity in the room, not just the ones where they participate). With 2.18, we introduced the concept of “**user** notification settings”, which are **project-level** settings that control the notifications ( [`"notification"` webhook events](/docs/platform/webhooks#NotificationEvent), not "inbox notifications") received by a user (e.g. setting `email.$myCustomNotification` to `false` would disable `"notification"` webhook events for the `$myCustomNotification` kind and the `email` channel). To improve clarity, we’re renaming these concepts: - “room **notification** settings” → “room **subscription** settings”: they control which things a user is subscribed to in a room - “**user** notification settings” → “notification settings”: they control which notifications a user receives ### What changed All methods, hooks, types, and REST API endpoints that were using these terms have been renamed to use the new naming, see the examples below. We provide a codemod to automatically update your codebase to the new naming. ```bash npx @liveblocks/codemod@latest rename-notification-settings ``` #### `@liveblocks/react` ```tsx // ❌ Before import { // +++ useRoomNotificationSettings, useUpdateRoomNotificationSettings, // +++ } from "@liveblocks/react"; // ✅ After import { // +++ useRoomSubscriptionSettings, useUpdateRoomSubscriptionSettings, // +++ } from "@liveblocks/react"; ``` ```tsx // ❌ Before useErrorListener((error) => // +++ if (error.context.type === "UPDATE_NOTIFICATION_SETTINGS_ERROR") { // +++ /* ... */ } ); // ✅ After useErrorListener((error) => // +++ if (error.context.type === "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR") { // +++ /* ... */ } ); ``` #### `@liveblocks/client` ```tsx // ❌ Before import type { RoomNotificationSettings } from "@liveblocks/client"; // ✅ After import type { RoomSubscriptionSettings } from "@liveblocks/client"; ``` ```tsx // ❌ Before const { room, leave } = client.enterRoom("my-room-id"); // +++ room.getNotificationSettings(); room.updateNotificationSettings(/* ... */); // +++ // ✅ After const { room, leave } = client.enterRoom("my-room-id"); // +++ room.getSubscriptionSettings(); room.updateSubscriptionSettings(/* ... */); // +++ ``` ```tsx // ❌ Before import type { UserNotificationSettings } from "@liveblocks/client"; // ✅ After import type { NotificationSettings } from "@liveblocks/client"; ``` #### `@liveblocks/node` ```tsx // ❌ Before const liveblocks = new Liveblocks({ secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx", }); // +++ await liveblocks.getRoomNotificationSettings(/* ... */); await liveblocks.updateRoomNotificationSettings(/* ... */); await liveblocks.deleteRoomNotificationSettings(/* ... */); // +++ // ✅ After const liveblocks = new Liveblocks({ secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx", }); // +++ await liveblocks.getRoomSubscriptionSettings(/* ... */); await liveblocks.updateRoomSubscriptionSettings(/* ... */); await liveblocks.deleteRoomSubscriptionSettings(/* ... */); // +++ ``` #### REST API ```shell # ❌ Before https://api.liveblocks.io/v2/rooms/:roomId/users/:userId/notification-settings # ✅ After https://api.liveblocks.io/v2/rooms/:roomId/users/:userId/subscription-settings ``` That’s it for 2.24! [`@liveblocks/client`]: /docs/api-reference/liveblocks-client [`@liveblocks/react`]: /docs/api-reference/liveblocks-react [`@liveblocks/node`]: /docs/api-reference/liveblocks-node --- meta: title: "Upgrading to 2.8" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 2.8" --- We are introducing attachments to allow users to add files to their comments. ## How to upgrade? [#how] You can upgrade to 2.8 by downloading the latest version of each Liveblocks package you’re using. The easiest way to do that is to run the following command: ```bash npx liveblocks@latest upgrade ``` ## All changes are for Comments If you are not using Comments or are not using [the default Comments components](/docs/ready-made-features/comments/default-components), there are no breaking changes for you! However, if you are, or intend to use them later, keep reading. ## Attachments Liveblocks 2.8 adds attachments to Comments, and while not a breaking change, it is mostly enabled by default in the default components. We recommend you either [enable attachments](#enable-attachments) or [disable attachments](#disable-attachments) when you upgrade. ### Enable attachments [#enable-attachments] Attachments are enabled by default, but if you’re using [`useCreateThread`](/docs/api-reference/liveblocks-react#useCreateThread), [`useCreateComment`](/docs/api-reference/liveblocks-react#useCreateComment), or [`useEditComment`](/docs/api-reference/liveblocks-react#useEditComment), you can now pass these an attachments array. ```tsx const createThread = useCreateThread(); // ❌ Before - Liveblocks 2.7 createThread({ body: {}, metadata: {} }); // ✅ After - Liveblocks 2.8 createThread({ body: {}, attachments: [], metadata: {} }); ``` These hooks are most commonly used to enable [custom Composer behavior](/docs/api-reference/liveblocks-react-ui#Custom-behavior) with `onComposerSubmit`, which now provides the `attachments` array for you. You must pass this to your mutations for attachments to work correctly. ```tsx const createThread = useCreateThread(); // ❌ Before - Liveblocks 2.7 { event.preventDefault(); createThread({ body, metadata: {} }); }} /> // ✅ After - Liveblocks 2.8 { // +++ event.preventDefault(); // +++ createThread({ body, attachments, metadata: {} }); // +++ }} /> ``` Remember that this applies to [`useCreateComment`](/docs/api-reference/liveblocks-react#useCreateComment) and [`useEditComment`](/docs/api-reference/liveblocks-react#useEditComment) too, not just [`useCreateThread`](/docs/api-reference/liveblocks-react#useCreateThread). No further changes are necessary to enable attachments in the default components. {/* TODO If you’re using primitives you must ... */} ### Disable attachments [#disable-attachments] If you’d prefer to disable attachments, you can do so by setting the `showAttachments` prop to `false` on each of the following components: [`Composer`](/docs/api-reference/liveblocks-react-ui#Composer), [`Comment`](/docs/api-reference/liveblocks-react-ui#Comment), [`Thread`](/docs/api-reference/liveblocks-react-ui#Thread), and [`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification.Thread). ```tsx // Disable attachments ``` ## Default Composer component structure The default [`Composer`](/docs/api-reference/liveblocks-react-ui#Composer) component’s structure has slightly changed, so if you customized its styles: make sure to check if and how the new structure affects your changes. ```html
``` --- meta: title: "Upgrading to 2.9" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 2.9" --- We are introducing pagination to Comments and Notifications as default. You need to upgrade your app to handle this. ## How to upgrade? [#how] You can upgrade to 2.9 by downloading the latest version of each Liveblocks package you’re using. The easiest way to do that is to run the following command: ```bash npx liveblocks@latest upgrade ``` ## All changes are for Comments & Notifications If you’re not using [`useThreads`](/docs/api-reference/liveblocks-react#useThreads) or [`useInboxNotifications`](/docs/api-reference/liveblocks-react#useInboxNotifications) there are no breaking changes for you! However, if you are using them, keep reading. ## Pagination [`useThreads`](/docs/api-reference/liveblocks-react#useThreads) and [`useInboxNotifications`](/docs/api-reference/liveblocks-react#useInboxNotifications) now only fetch the **latest 50 threads/notifications**, and you must use paginate to retrieve more. Previously, these two functions would fetch every single thread/notification, but this is no longer possible. If your rooms have fewer than 50 threads, and your users fewer than 50 notifications, there will be no visible difference when upgrading. Pagination is only used after 50. ### Threads Here’s a before and after example with [`useThreads`](/docs/api-reference/liveblocks-react#useThreads), adding pagination. ```tsx title="Before" import { useThreads } from "@liveblocks/react/suspense"; function Threads() { const { threads } = useThreads(); return (
{threads.map((thread) => ( ))}
); } ``` ```tsx title="After" import { useThreads } from "@liveblocks/react/suspense"; function Threads() { const { threads, // +++ fetchMore, isFetchingMore, hasFetchedAll, fetchMoreError, // +++ } = useThreads(); // +++ const loadMore = fetchMoreError ? ( <>

Error loading more threads: {fetchMoreError.message}

) : ( ); // +++ return (
{threads.map((thread) => ( ))} // +++ {hasFetchedAll ?
🎉 You're all caught up!
: loadMore} // +++
); } ``` ### Notifications Here’s a before and after example with [`useInboxNotifications`](/docs/api-reference/liveblocks-react#useInboxNotifications), adding pagination. ```tsx title="Before" import { useInboxNotifications } from "@liveblocks/react/suspense"; function Notifications() { const { inboxNotifications } = useInboxNotifications(); return (
{inboxNotifications.map((notification) => ( ))}
); } ``` ```tsx title="After" import { useInboxNotifications } from "@liveblocks/react/suspense"; function Notifications() { const { inboxNotifications, // +++ fetchMore, isFetchingMore, hasFetchedAll, fetchMoreError, // +++ } = useInboxNotifications(); // +++ const loadMore = fetchMoreError ? ( <>

Error loading more notifications: {fetchMoreError.message}

) : ( ); // +++ return (
{inboxNotifications.map((notification) => ( ))} // +++ {hasFetchedAll ?
🎉 You're all caught up!
: loadMore} // +++
); } ``` ### Learn more Learn more about pagination under [`useThreads`](/docs/api-reference/liveblocks-react#useThreads) and [`useInboxNotifications`](/docs/api-reference/liveblocks-react#useInboxNotifications). --- meta: title: "Upgrading to 3.0" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 3.0" --- Liveblocks 3.0 is our third major release, focusing on our newest product, [AI Copilots](https://liveblocks.io/blog/meet-liveblocks-3-0-the-fastest-way-to-let-your-users-collaborate-with-ai-in-your-product). We’ve used this as an opportunity to tidy up some of our existing APIs, ensuring consistency throughout our offering. ## How to upgrade [#how-to-upgrade] First of all, let’s upgrade all Liveblocks dependencies to their latest versions. The easiest way to do that is to run the following command: ```bash npx liveblocks@latest upgrade ``` There are some **breaking changes** in this update. ## Does this affect you? [#does-this-affect-you] **If you’re using TypeScript 4.9 or lower**, TypeScript 5.0 is now the minimum supported version. **If you’re using [Comments](/docs/ready-made-features/comments) and/or [Text Editor](/docs/ready-made-features/text-editor)**, please read about [changes to mentions](#mentions). **If you’re using [Notifications](/docs/ready-made-features/notifications)**, please read about [a change related to notification settings](#notification-settings). **If you’re using [`@liveblocks/emails`](/docs/api-reference/liveblocks-emails)**, please read about [changes to its returned values](#liveblocks-emails). **If you’re using [`LiveblocksUIConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig)**, please read about [its renaming](#liveblocks-ui-config). **If you’re using any deprecated APIs** (e.g. `useBatch`, `useStorageStatus`, `useEditorStatus`…), please read about [the ones being removed](#deprecated). Otherwise, you can simply upgrade your packages and no changes will affect you. ## Mentions [#mentions] We’re making some changes to mentions in Comments and Text Editor to support different mention kinds in future releases: user mentions, group mentions, etc. ### Components [#mentions-components] The `onMentionClick` prop on [`Thread`][] and [`Comment`][] now receives a `MentionData` object as its first argument instead of a `userId` string. ```tsx // ❌ Before onMentionClick(userId: string, event: MouseEvent) => void; // userId: "user-0" // ✅ After onMentionClick(mention: MentionData, event: MouseEvent) => void; // mention: { kind: "user", id: "user-0" } ``` ### Primitives [#mentions-primitives] When customizing the [`Mention`](/docs/api-reference/liveblocks-react-ui#primitives-Comment.Body-Mention) component on the [`Comment.Body`][] and [`Composer.Editor`][] primitives, the `userId` prop has been replaced by a `mention` one which is a `MentionData` object. ```tsx import { Comment, Composer } from "@liveblocks/react-ui/primitives"; // ❌ Before @{userId}, // +++ }} />; ( @{userId} ), // +++ }} />; // ✅ After @{mention.id}, // +++ }} />; ( @{mention.id} ), // +++ }} />; ``` When customizing the [`MentionSuggestions`](/docs/api-reference/liveblocks-react-ui#primitives-Composer.Editor-MentionSuggestions) component on the [`Composer.Editor`][] primitive, the `userIds` prop has been replaced by a `mentions` one which is an array of `MentionData` objects, and the `selectedUserId` prop has been renamed to `selectedMentionId`. ```tsx import { Composer } from "@liveblocks/react-ui/primitives"; // ❌ Before ( // +++ // +++ {userIds.map((userId) => ( {userId} ))} // +++ ), }} />; // ✅ After ( // +++ // +++ {mentions.map((mention) => ( {mention.id} ))} // +++ ), }} />; ``` ### `@liveblocks/emails` [#mentions-liveblocks-emails] The [`prepareTextMentionNotificationEmailAsReact`][] and [`prepareTextMentionNotificationEmailAsHtml`][] functions’ returned data changed slightly: - The `id` property is now named `textMentionId`, it refers to the mention’s Text Mention ID, not the user ID used for the mention - The `id` property now refers to the mention’s ID, as in the user ID used for the mention - The `kind` property now indicates the mention’s kind (e.g. `"user"`, `"group"`, etc.) ```tsx await prepareTextMentionNotificationEmailAsReact(liveblocks, event); // ❌ Before // { // mention: { // +++ // id: "in_xxx", // +++ // roomId: "123", // createdAt: new Date(), // author: { // id: "user-who-created-the-mention", // info: { // name: "Aurélien", // }, // }, // content: /* The rendered content */, // }, // ... // } // ✅ After // { // mention: { // +++ // textMentionId: "in_xxx", // id: "user-who-is-mentioned", // kind: "user", // +++ // roomId: "123", // createdAt: new Date(), // author: { // id: "user-who-created-the-mention", // info: { // name: "Aurélien", // }, // }, // content: /* The rendered content */, // }, // ... // } ``` When customizing the [`Mention`](/docs/api-reference/liveblocks-emails#prepare-text-mention-notification-email-as-react-customizing-components) component in the [`prepareTextMentionNotificationEmailAsReact`][] function, the `element` prop’s `userId` property has been renamed to `id`, and `element.kind` now indicates the mention’s kind. ```tsx await prepareThreadNotificationEmailAsReact(liveblocks, webhookEvent, { components: { // ❌ Before Mention: ({ element, user }) => ( // +++ @{element.userId} // +++ ), // ✅ After Mention: ({ element, user }) => ( // +++ @{element.id} // +++ ), }, }); ``` ### Utilities [#utilities] The `getMentionedIdsFromCommentBody` utility (from [`@liveblocks/client`](/docs/api-reference/liveblocks-client) and [`@liveblocks/node`](/docs/api-reference/liveblocks-node)) has been replaced by [`getMentionsFromCommentBody`](/docs/api-reference/liveblocks-client#get-mentions-from-comment-body). ```tsx // ❌ Before getMentionedIdsFromCommentBody(commentBody); // ["user-1", "user-2"] // ✅ After getMentionsFromCommentBody(commentBody, (mention) => mention.kind === "user"); // [{ type: "mention", kind: "user", id: "user-1" }, { type: "mention", kind: "user", id: "user-2" }] ``` By default, if the optional second argument is not provided, all mentions are returned, including future mention kinds (e.g. group mentions in the future). ```tsx // All mentions getMentionsFromCommentBody(commentBody); // Only user mentions getMentionsFromCommentBody(commentBody, (mention) => mention.kind === "user"); ``` ## Notification settings [#notification-settings] 2.24 [introduced a few naming changes](/docs/platform/upgrading/2.24) around the concepts of “notification settings” and “subscription settings” to improve clarity. 3.0 [removes the aliases for the previous names](#deprecated) but it also introduces one change that we couldn’t make in 2.24: The `UPDATE_USER_NOTIFICATION_SETTINGS_ERROR` error has been renamed to `UPDATE_NOTIFICATION_SETTINGS_ERROR`. ```tsx // ❌ Before useErrorListener((error) => // +++ if (error.context.type === "UPDATE_USER_NOTIFICATION_SETTINGS_ERROR") { // +++ /* ... */ } ); // ✅ After useErrorListener((error) => // +++ if (error.context.type === "UPDATE_NOTIFICATION_SETTINGS_ERROR") { // +++ /* ... */ } ); ``` ## `@liveblocks/emails` [#liveblocks-emails] The functions to prepare HTML/React emails are now more consistent, using the same `body` and `content` properties instead of `reactBody`/`htmlBody` and `reactContent`/`htmlContent`. ```tsx // ❌ Before prepareThreadNotificationEmailAsReact(liveblocks, event); // { comment: { reactBody: ReactNode, ... }, ... } prepareThreadNotificationEmailAsHtml(liveblocks, event); // { comment: { htmlBody: string, ... }, ... } prepareTextMentionNotificationEmailAsReact(liveblocks, event); // { mention: { reactContent: ReactNode, ... }, ... } prepareTextMentionNotificationEmailAsHtml(liveblocks, event); // { mention: { htmlContent: string, ... }, ... } // ✅ After prepareThreadNotificationEmailAsReact(liveblocks, event); // { comment: { body: ReactNode, ... }, ... } prepareThreadNotificationEmailAsHtml(liveblocks, event); // { comment: { body: string, ... }, ... } prepareTextMentionNotificationEmailAsReact(liveblocks, event); // { mention: { content: ReactNode, ... }, ... } prepareTextMentionNotificationEmailAsHtml(liveblocks, event); // { mention: { content: string, ... }, ... } ``` ## `LiveblocksUiConfig` [#liveblocks-ui-config] The [`LiveblocksUIConfig`](/docs/api-reference/liveblocks-react-ui#LiveblocksUiConfig) utility has been renamed to `LiveblocksUiConfig` (`UI` → `Ui`) for consistency with other Liveblocks APIs. Run the following **codemod** or manually make the changes: ```bash npx @liveblocks/codemod@latest liveblocks-ui-config ``` ## Removed deprecated APIs [#deprecated] All of the following APIs have been removed in 3.0, as they were deprecated multiple versions ago. **Affecting all packages:** - The `code` property has been removed from `LiveblocksError`, use `LiveblocksError.context.code` instead for better TypeScript support. - The deprecated aliases from the [2.24 naming changes](/docs/platform/upgrading/2.24) have been removed, use their new names instead. There’s a codemod available to automatically use the new names: `npx @liveblocks/codemod@latest rename-notification-settings`. **Affecting `@liveblocks/client`:** - The `unstable_fallbackToHTTP` client option has been removed, set `largeMessageStrategy` to `"experimental-fallback-to-http"` instead. **Affecting `@liveblocks/node`:** - The `metadata` option has been removed from the [`getRooms`][] method, use `query.metadata` instead. - The `nextPage` property has been removed from the [`getRooms`][] method’s response, use `query.nextPage` instead. - The `RoomData` type has been removed, use `RoomInfo` instead. There’s a codemod available to automatically use the new type: `npx @liveblocks/codemod@latest room-info-to-room-data`. **Affecting `@liveblocks/react`:** - The `useBatch` hook has been removed, use [`useMutation`][] instead for writing to Storage, which will automatically batch all mutations. - The `useStorageStatus` hook has been removed, use [`useSyncStatus`][] instead for tracking sync status, it reflects the sync status of all parts of Liveblocks. **Affecting `@liveblocks/react-lexical`:** - The `useEditorStatus` hook has been removed, use [`useSyncStatus`][] instead for tracking sync status, it reflects the sync status of all parts of Liveblocks. [`Thread`]: /docs/api-reference/liveblocks-react-ui#Thread [`Comment`]: /docs/api-reference/liveblocks-react-ui#Comment [`Comment.Body`]: /docs/api-reference/liveblocks-react-ui#primitives-Comment.Body [`Composer.Editor`]: /docs/api-reference/liveblocks-react-ui#primitives-Composer.Editor [`prepareTextMentionNotificationEmailAsReact`]: /docs/api-reference/liveblocks-emails#prepare-text-mention-notification-email-as-react [`prepareTextMentionNotificationEmailAsHtml`]: /docs/api-reference/liveblocks-emails#prepare-text-mention-notification-email-as-html [`getRooms`]: /docs/api-reference/liveblocks-node#get-rooms [`useSyncStatus`]: /docs/api-reference/liveblocks-react#useSyncStatus [`useMutation`]: /docs/api-reference/liveblocks-react#useMutation --- meta: title: "Upgrading to 3.10" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 3.10" --- Liveblocks 3.10 releases breaking changes for [Tiptap](#Upgrade-Tiptap) and [BlockNote](#Upgrade-BlockNote) users, as it requires at least Tiptap 3.0 or BlockNote 0.39. No other parts of Liveblocks are affected by breaking changes. ## Not using Tiptap or BlockNote? **If you’re not using Tiptap or BlockNote, there are no breaking changes**, simply upgrade all Liveblocks dependencies to their latest versions. The easiest way to upgrade is to run the following command: ```bash npx liveblocks@latest upgrade ``` ## Upgrade Tiptap Tiptap v3 is a major update with breaking changes, but the migration process is quite simple. First, you _must_ uninstall all Tiptap and Liveblocks dependencies to avoid future version conflicts as we upgrade. This includes any extra Tiptap extensions you may have installed or anything else with a dependency on Tiptap. ### AI-assisted upgrade The easiest way to upgrade is to use our AI assistant to generate the uninstall and install commands for you. Make sure you’re signed in, paste your `package.json` into the following input, and generate the commands. Generate commands Run the two commands then skip to [Critical changes for Liveblocks users](#Critical-changes-for-Liveblocks-Tiptap-users). ### Manual upgrade If you’d like to upgrade manually, you need to run the npm command to uninstall all Tiptap and Liveblocks packages. Note that you are likely using more package than are listed here, such as Tiptap extensions. ```bash npm uninstall @tiptap/react @tiptap/starter-kit @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/react-tiptap @liveblocks/node ``` Next re-install all Tiptap and Liveblocks packages. If you see a peer dependency issue during this install, you may have missed something during the previous uninstall step. ```bash npm install @tiptap/react@3 @tiptap/starter-kit@3 @liveblocks/client@latest @liveblocks/react@latest @liveblocks/react-ui@latest @liveblocks/react-tiptap@latest @liveblocks/node@latest ``` This will ensure that all packages are compatible with each other and prevent version conflicts. ### Critical changes for Liveblocks Tiptap users While Tiptap 3 includes many changes, there are two particularly critical changes that affect Liveblocks integration: #### StarterKit configuration change Liveblocks provides its own undo/redo functionality for collaborative editing. In Tiptap 2, you disabled this by setting `history: false` in the StarterKit extension. In Tiptap 3, this option has been renamed to `undoRedo`: ```diff const editor = useEditor({ extensions: [ StarterKit.configure({ - history: false, + undoRedo: false, }), // Other extensions... ], }); ``` #### Set `immediatelyRender: false` for SSR If you're using server-side rendering (such as with Next.js), it's now more critical to set `immediatelyRender: false` in your editor configuration to prevent hydration issues: ```tsx highlight="3" const editor = useEditor( { immediatelyRender: false, extensions: [ StarterKit.configure({ undoRedo: false, }), // Other extensions... ], }, [] ); ``` This ensures the editor doesn't render on the server, which can cause mismatches between server and client rendering. #### Style Changes If you have your own styles for collaboration cursors, the default CSS classes prefix changed from `.collaboration-cursor` to `.collaboration-carets`. ```diff - .collaboration-cursor__caret { + .collaboration-carets__caret { /* Your caret styles */ } - .collaboration-cursor__label { + .collaboration-carets__label { /* Your label styles */ } ``` ### Full migration guide For a complete list of breaking changes and new features in Tiptap 3, refer to the [official Tiptap upgrade guide](https://tiptap.dev/docs/guides/upgrade-tiptap-v2). ## Upgrade BlockNote BlockNote 0.39 has upgraded to use Tiptap 3.0 under the hood, which requires a simple upgrade. First, you _must_ uninstall all BlockNote, Tiptap, and Liveblocks dependencies to avoid future version conflicts as we upgrade. This includes any extra Tiptap extensions you may have installed or anything else with a dependency on Tiptap. ### AI-assisted upgrade The easiest way to upgrade is to use our AI assistant to generate the uninstall and install commands for you. Make sure you’re signed in, paste your `package.json` into the following input, and generate the commands. Generate commands Run the two commands then skip to [Critical changes for Liveblocks BlockNote users](#Critical-changes-for-Liveblocks-BlockNote-users). ### Manual upgrade If you’d like to upgrade manually, you need to run the npm command to uninstall all BlockNote, Tiptap, and Liveblocks packages. Note that you are likely using more package than are listed here, such as text editor extensions. ```bash npm uninstall @blocknote/core @blocknote/mantine @liveblocks/client @liveblocks/react @liveblocks/react-ui @liveblocks/react-blocknote @liveblocks/node ``` Next re-install all BlockNote, Tiptap, and Liveblocks packages. If you see a peer dependency issue during this install, you may have missed something during the previous uninstall step. ```bash npm install @blocknote/core@latest @blocknote/mantine@latest @liveblocks/client@latest @liveblocks/react@latest @liveblocks/react-ui@latest @liveblocks/react-blocknote@latest @liveblocks/node@latest ``` This will ensure that all packages are compatible with each other and prevent version conflicts. ### Critical changes for Liveblocks BlockNote users #### Style Changes If you have your own styles for collaboration cursors, the default CSS classes prefix changed from `.collaboration-cursor` to `.collaboration-carets`. ```diff - .collaboration-cursor__caret { + .collaboration-carets__caret { /* Your caret styles */ } - .collaboration-cursor__label { + .collaboration-carets__label { /* Your label styles */ } ``` ### BlockNote release notes To learn more about the changes in BlockNote 0.39, please refer to the [BlockNote 0.39 release notes](https://github.com/TypeCellOS/BlockNote/releases/tag/v0.39.0). --- meta: title: "Upgrading to 3.18" parentTitle: "Upgrading" description: "Guide to upgrade to Liveblocks version 3.18" --- Liveblocks 3.18 introduces a new JSON-based serialization model for Live structures. `.toJSON()` replaces `.toImmutable()` as the canonical way to read Storage snapshots, and `useStorage` now returns plain objects instead of `Map` instances for `LiveMap` values. ## Why this change? [#why-this-change] Previously, `useStorage` returned `Map` instances for `LiveMap` values, while `LiveObject` values were returned as plain objects. This difference caused practical problems in real apps: because `Map`s cannot be serialized with `JSON.stringify()`, the result of `useStorage()` could never be assumed to be legal JSON, even though many users expected that. Instead, LiveMaps would appear as empty objects when serialized to JSON. By unifying the return value of `useStorage()` for every Live structure to JSON, users can now "just" store the results of `useStorage()` where they want, pass it around, and serialize it without worrying about the underlying structure. The downside however, is a syntactically breaking change for `LiveMap` users: instead of `Map` instances, these now appear as plain objects. However, since the change is purely syntactical and mechanical, no real functionality is lost: every read-only `Map` operation has an equivalent plain-object expression. See the [migration table](#livemap-values-in-usestorage) below for a complete list of rewrites. ## How to upgrade [#how-to-upgrade] First of all, let's upgrade all Liveblocks dependencies to their latest versions. The easiest way to do that is to run the following command: ```bash npx liveblocks@latest upgrade ``` There are some potentially **breaking changes** in this update. ## Does this affect you? [#does-this-affect-you] **If you're reading `LiveMap` values from `useStorage`**, the return type has changed from `Map` to a plain object. See [LiveMap values in useStorage](#livemap-values-in-usestorage). **If you're using `.toImmutable()` on any Live structure**, it has been removed. See [Removed .toImmutable()](#removed-toimmutable). **If you're using `.toObject()` on `LiveObject`**, it has been removed. See [Removed .toObject()](#removed-toobject). **If you're using `.toArray()` on `LiveList`**, it has been removed. See [Removed .toArray()](#removed-toarray). **If you're using the `ToImmutable` type**, it has been removed. See [Removed ToImmutable type](#removed-toimmutable-type). Otherwise, you can simply upgrade your packages and no changes will affect you. ## LiveMap values in useStorage [#livemap-values-in-usestorage] `useStorage` now returns plain objects for `LiveMap` values instead of `Map` instances. If you're accessing `LiveMap` data through `useStorage`, you'll need to update your code. The change is fully mechanical. Every `Map` method has a direct plain-object equivalent: ```tsx const myMap = useStorage((root) => root.myMap); ``` | ❌ Before | ✅ After | | ----------------------------- | --------------------------- | | `myMap.get("my-node")` | `myMap["my-node"]` | | `myMap.has("my-node")` | `"my-node" in myMap` | | `myMap.size` | `Object.keys(myMap).length` | | `Array.from(myMap.keys())` | `Object.keys(myMap)` | | `Array.from(myMap.values())` | `Object.values(myMap)` | | `Array.from(myMap.entries())` | `Object.entries(myMap)` | Of course, if you prefer, you can still convert the plain object back to a `Map` if your application requires it: ```tsx // Still possible, but not recommended const myMap = useStorage((root) => root.myMap); const myTrueMap = useMemo(() => new Map(Object.entries(myMap)), [myMap]); ``` ## Removed .toImmutable() [#removed-toimmutable] `.toImmutable()` has been removed from all Live structures (`LiveObject`, `LiveList`, `LiveMap`). Use `.toJSON()` instead, which returns a cached, JSON-compatible snapshot. If you aren't using any `LiveMap`s in your application, the results will be identical. ```tsx // ❌ Before const snapshot = liveObject.toImmutable(); // ✅ After const snapshot = liveObject.toJSON(); ``` ## Removed .toObject() [#removed-toobject] `.toObject()` has been removed from `LiveObject`. Use `.toJSON()` or `.get(key)` instead: ```tsx // ❌ Before const obj = liveObject.toObject(); // ✅ After const obj = liveObject.toJSON(); // or access individual keys const value = liveObject.get("key"); ``` ## Removed .toArray() [#removed-toarray] `.toArray()` has been removed from `LiveList`. Use `.toJSON()` or iterate directly: ```tsx // ❌ Before const arr = liveList.toArray(); // ✅ After const arr = liveList.toJSON(); // or iterate directly for (const item of liveList) { // ... } ``` ## Removed ToImmutable type [#removed-toimmutable-type] The `ToImmutable` type has been removed. Use `ToJson` instead: ```tsx // ❌ Before import type { ToImmutable } from "@liveblocks/client"; type Snapshot = ToImmutable; // ✅ After import type { ToJson } from "@liveblocks/client"; type Snapshot = ToJson; ``` --- meta: title: "Upgrading Liveblocks" description: "Select the Liveblocks version you’d like to update to" --- Select the version you’d like to update to. When upgrading, please note that all packages should be upgraded to the same version. If a version isn’t mentioned, there are no breaking changes. 3.18} /> 3.10} /> 3.0} /> 2.24} /> 2.16} /> 2.15} /> 2.9} /> 2.8} /> 2.2} /> 2.0} /> 1.10} /> 1.9} /> 1.5} /> 1.2} /> 1.0} /> 0.19} /> 0.18} /> 0.17} /> --- meta: title: "The new realtime data storage engine and its benefits" description: "Learn about our improved v2 realtime data storage engine and its benefits" --- As of May 2026, every Liveblocks room runs on the v2 realtime data storage engine. The background migration of existing v1 rooms is finished—there is nothing you need to do. Since v3.14, rooms are powered by our new v2 realtime data storage engine—a ground-up rearchitecture that removes server-side memory limits, enabling faster initial loads and support for much larger documents. The switch was **seamless and required no code changes**. The two sync engines we support—[Liveblocks Storage](/docs/ready-made-features/multiplayer/sync-engine/liveblocks-storage) and [Yjs](/docs/ready-made-features/multiplayer/sync-engine/liveblocks-yjs)—both run on top of the v2 engine. ## What are the benefits? The main architectural leap of v2 is that the Liveblocks server no longer needs to keep the entire document in memory. In v1, large documents put memory pressure on our edge workers, which are limited in capacity. A 50 MiB document, including its overhead, could easily cause an OOM crash in an edge worker on the v1 engine, in turn causing the room to become inaccessible. Rooms on the v2 realtime data storage engine now stream their document data directly from our persisted storage layer, keeping only minimal data in memory. Conflict resolution and mutations happen directly in the storage layer too. This enables arbitrarily large documents—server memory is no longer the bottleneck (browser memory, network speed, or your app’s rendering performance might now be). Initial load time will be faster, especially for larger documents: the server can start streaming immediately without buffering, and our wire protocol has been compacted to reduce transmission overhead. Several [previously documented platform limits](/docs/platform/limits) have been raised significantly for rooms on the v2 engine: | Item | v1 limit | v2 limit | | ----------------------- | -------- | -------- | | Broadcast event message | 1 MB | 32 MB | | `LiveObject` | 128 kB | 2 MB | | `LiveMap` value | 128 kB | 2 MB | | `LiveList` value | 128 kB | 2 MB | ## How it works Since March 10, 2026, all newly created rooms automatically use the v2 engine. No opt-in or code changes are required. If you're on an older SDK version, we recommend upgrading to v3.14+ to take full advantage of the v2 engine: `npx liveblocks@latest upgrade` ### Migrating existing rooms Between April 15 and May 2026, we transparently migrated all existing v1 room data over to the v2 engine in the background. **The migration is now complete**, and every room across the platform runs on v2. --- meta: title: "Adding Liveblocks to existing useState hooks" description: "Learn how to add Liveblocks to your existing useState hooks" --- Note that this is not the recommended way to build your app. We recommend using conflict-free data types and the `useStorage` and `useMutation` hooks, to take full advantage of our features. You can easily add Liveblocks to an existing `useState` hook by broadcasting and listening to events. ```ts import { useState } from "react"; import { useBroadcastEvent, useEventListener } from "./liveblocks.config"; function useCustomState() { const [state, setState] = useState(); const broadcast = useBroadcastEvent(); // Update useState and broadcast an event const setStateAndBroadcast = (newValue) => { setState(newValue); broadcast({ type: "STATE_UPDATE", data: newValue }); }; // Listen for the broadcast event useEventListener(({ event }) => { if (event.type === "STATE_UPDATE") { setState(event.data); } }); return [state, setStateAndBroadcast]; } ``` --- meta: title: "Can I use my own database with Yjs?" description: "Learn how to use webhooks to duplicate Yjs document data to your own database" --- When building collaborative applications with Yjs and Liveblocks, you may wish to store document data in your own database alongside Liveblocks. While Liveblocks needs to store Yjs document data to handle realtime collaboration, you can use webhooks to duplicate this data to your own database whenever changes occur. ## Why Liveblocks stores Yjs data Liveblocks must store Yjs document data to provide realtime collaboration features. This storage is essential for: - **Realtime synchronization**: Distributing changes to all connected users instantly. - **Conflict resolution**: Merging changes from multiple users working simultaneously. - **Offline support**: Syncing changes when users reconnect after being offline. - **Document loading**: Loading the current document state when a user enters the room. Without storing this data, Liveblocks wouldn't be able to provide the realtime collaboration features that make Yjs powerful. However, you can still maintain your own copy of the data for additional purposes like backups, analytics, or creating full-text search. ## Duplicating data with webhooks The best way to keep your own database in sync with Liveblocks is to use the [`YDocUpdated`](/docs/platform/webhooks#YDocUpdatedEvent) webhook event. This event fires whenever a Yjs document changes, allowing you to copy the updated data to your database. Your server can then: 1. Receive the webhook event. 2. Fetch the latest document data from Liveblocks. 3. Store it in your database. This approach ensures your database stays synchronized with Liveblocks without interfering with realtime collaboration. ### Webhook throttling Yjs documents can update up to 60 times per second during active collaboration, so the `YDocUpdated` webhook is throttled to prevent overwhelming your server. The webhook event will run no more than once every 60 seconds by default, but on Enterprise plans, you can configure this throttle to be as low as every 5 seconds, giving you near realtime synchronization to your database. If you’re interested in lowering the throttle value, [reach out to our team](/contact/sales) to discuss your needs. ## Setting up database synchronization We have detailed guides that walk you through how to set up your webhooks and store the Yjs data in your database. - [How to synchronize your Liveblocks Yjs document data to a Vercel Postgres database](/docs/guides/how-to-synchronize-your-liveblocks-yjs-document-data-to-a-vercel-postgres-database) - [How to synchronize your Liveblocks Yjs document data to a Supabase Postgres database](/docs/guides/how-to-synchronize-your-liveblocks-yjs-document-data-to-a-supabase-postgres-database) - [How to synchronize your Liveblocks Yjs document data to a PlanetScale MySQL database](/docs/guides/how-to-synchronize-your-liveblocks-yjs-document-data-to-a-planetscale-mysql-database) If your database isn’t mentioned in these guides, the process is still similar, so we recommend following the general steps outlined in the guides. The guides above detail fetching your Yjs data, however if you’re using one of our text editor extensions, you can use our server packages to fetch the Tiptap or Lexical data instead. ### Tiptap If you’re using Tiptap, [`withProsemirrorDocument`](/docs/api-reference/liveblocks-node-prosemirror#withProsemirrorDocument) can be used to fetch the Tiptap data, as text, markdown, JSON, and more, when receiving the webhook event. ```ts const textContent = await withProsemirrorDocument( { roomId: "my-room-id", client: liveblocks }, async (api) => { return api.toMarkdown(); } ); // "# Hello world" console.log(textContent); ``` Additionally you can modify the live document with [`setContent`](/docs/api-reference/liveblocks-node-prosemirror#api.setContent). ### Lexical If you’re using Lexical, [`withLexicalDocument`](/docs/api-reference/liveblocks-node-lexical#withLexicalDocument) can be used to fetch the Lexical data, as text, markdown, JSON, and more, when receiving the webhook event. ```ts const textContent = await withLexicalDocument( { roomId: "my-room-id", client: liveblocks }, async (doc) => { return doc.toMarkdown(); } ); // "# Hello world" console.log(textContent); ``` Additionally you can modify the live document with [`update`](/docs/api-reference/liveblocks-node-lexical#doc.update). ## Learn more For more information about data storage and webhooks, check out these resources: ### API documentation Learn more about the APIs for working with Yjs data: - [YDocUpdated webhook event](/docs/platform/webhooks#YDocUpdatedEvent) - [@liveblocks/node-prosemirror](/docs/api-reference/liveblocks-node-prosemirror) - [@liveblocks/node-lexical](/docs/api-reference/liveblocks-node-lexical) - [Data storage](/docs/platform/data-storage) --- meta: title: "Enabling agentic workflows with Liveblocks" parentTitle: "Guides" description: "Use Liveblocks REST APIs to let AI agents show presence, modify storage, and participate as collaborators in your rooms." ---
Liveblocks agentic workflows
AI agents can participate as first-class collaborators inside Liveblocks rooms. Two REST API capabilities make this possible: - **Ephemeral Presence** (`POST /v2/rooms/{roomId}/presence`): Lets an agent appear in a room with a name, avatar, and custom presence data, with an auto-expiring TTL. - **JSON Patch** (`PATCH /v2/rooms/{roomId}/storage/json-patch`): Lets an agent modify [Storage](/docs/ready-made-features/multiplayer/sync-engine/liveblocks-storage) using the RFC 6902 standard, which is well understood by LLMs, making it straightforward for AI models to generate patch operations directly from natural language instructions. Both are language-agnostic HTTP endpoints, so agents built in Python, TypeScript, or any other language can use them without a native Liveblocks client. ## Making your agent visible with ephemeral presence [#ephemeral-presence] The [`POST /v2/rooms/{roomId}/presence`](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-presence) endpoint lets your agent set presence in a room just like a connected user would. The presence expires automatically once the TTL elapses, so the agent never lingers after it’s done. ```shell title="Endpoint" POST https://api.liveblocks.io/v2/rooms/{roomId}/presence ``` Authenticate with your project’s **secret key** in the `Authorization` header. ```shell "Authorization: Bearer {{SECRET_KEY}}" ``` The request body accepts: - `userId`: The agent’s stable identifier. - `data`: Any presence object you want connected clients to see. - `userInfo`: Optional name, avatar URL, and color for the agent. - `ttl`: How long (in seconds) the presence should live (minimum: 2, maximum: 3599). ```bash curl -X POST "https://api.liveblocks.io/v2/rooms/my-room/presence" \ -H "Authorization: Bearer {{SECRET_KEY}}" \ -H "Content-Type: application/json" \ -d '{ "userId": "ai-agent", "data": { "status": "thinking", "focusedField": "email" }, "userInfo": { "name": "AI Agent", "avatar": "https://example.com/ai-agent-avatar.png", "color": "#6366f1" }, "ttl": 60 }' ``` A `204` response means the presence was set successfully.
Avatar stack with AI agent
### Rendering the agent as an avatar [#agent-avatar] Because the agent’s presence is set server-side, it flows to all connected clients through the normal Liveblocks presence system. The agent appears in [`useOthers`](/docs/api-reference/liveblocks-react#useOthers) alongside real users, so existing avatar stack components work without any changes. ```tsx import { useOthers, useSelf } from "@liveblocks/react/suspense"; function AvatarStack() { const others = useOthers(); const self = useSelf(); return (
{others.map(({ connectionId, info }) => ( {info.name} ))} {self && {self.info.name}}
); } ``` The `userInfo.avatar` you pass to the presence endpoint populates `info.avatar` here, so the agent shows up with its own avatar.
AI agent input presence
### Highlighting form fields [#highlight-fields] Presence `data` can carry any shape you choose. A `focusedField` property, for example, lets the frontend highlight which input the agent is currently working on, giving users real-time insight into what the agent is doing. ```tsx import { useOthers } from "@liveblocks/react/suspense"; function FormField({ name, label }: { name: string; label: string }) { const agentFocus = useOthers((others) => others.find((o) => o.presence.focusedField === name) ); return (
{agentFocus && ( {agentFocus.info.name} is reviewing… )}
); } ``` As the agent moves between fields, call the presence endpoint again with an updated `focusedField` value. Each call resets the TTL, so the presence stays alive as long as the agent keeps working. ## Modifying Storage with JSON Patch [#json-patch] The [`PATCH /v2/rooms/{roomId}/storage/json-patch`](/docs/api-reference/rest-api-endpoints#patch-rooms-roomId-storage-json-patch) endpoint lets an agent write directly to a room’s Storage document over HTTP. The body is a JSON array of operations following the [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) specification. ```shell title="Endpoint" PATCH https://api.liveblocks.io/v2/rooms/{roomId}/storage/json-patch ``` JSON Patch is a well-established standard that LLMs already understand, which means you can ask a model to produce the patch operations directly from a natural language instruction without writing custom tooling or prompt engineering. Supported operations are `add`, `remove`, `replace`, `move`, `copy`, and `test`. If any operation fails, the whole patch is rejected and the document is left unchanged. For a full reference of all operations and error handling, see the [Modifying Storage via REST API with JSON Patch](/guides/modifying-storage-via-rest-api-with-json-patch) guide. The example below uses Python, which is common in agentic pipelines: ```python import requests ROOM_ID = "my-room" SECRET_KEY = "sk_prod_..." operations = [ {"op": "replace", "path": "/formData/email", "value": "verified@example.com"}, {"op": "add", "path": "/formData/status", "value": "reviewed"}, ] response = requests.patch( f"https://api.liveblocks.io/v2/rooms/{ROOM_ID}/storage/json-patch", json=operations, headers={"Authorization": f"Bearer {SECRET_KEY}"}, ) response.raise_for_status() # raises on 4xx / 5xx ``` A `200` response means all operations were applied. A `422` response means the patch failed; the response body includes an `error` code, a human-readable `message`, and an optional `suggestion`. ## End-to-end example: agent reviews a form [#end-to-end] The following example walks through an agent that reviews a multi-field form. It uses ephemeral presence to show users what it’s doing in real time, and JSON Patch to commit its changes to Storage. ```python import requests BASE = "https://api.liveblocks.io/v2" ROOM = "my-room" AGENT_ID = "ai-agent" HEADERS = {"Authorization": "Bearer sk_prod_...", "Content-Type": "application/json"} def set_presence(focused_field: str, status: str, ttl: int = 30): requests.post( f"{BASE}/rooms/{ROOM}/presence", json={ "userId": AGENT_ID, "data": {"focusedField": focused_field, "status": status}, "userInfo": { "name": "AI Agent", "avatar": "https://example.com/ai-agent-avatar.png", }, "ttl": ttl, }, headers=HEADERS, ) def patch_storage(ops: list): requests.patch( f"{BASE}/rooms/{ROOM}/storage/json-patch", json=ops, headers=HEADERS, ).raise_for_status() # Step 1 — signal intent on the "email" field set_presence("email", "reviewing") # Step 2 — validate and update email in Storage patch_storage([ {"op": "replace", "path": "/formData/email", "value": "verified@example.com"}, ]) # Step 3 — move to the "name" field set_presence("name", "reviewing") patch_storage([ {"op": "replace", "path": "/formData/name", "value": "Jane Doe"}, ]) # Step 4 — mark the review as complete # Set a short TTL so presence expires soon after we're done set_presence("", "done", ttl=5) patch_storage([ {"op": "add", "path": "/formData/reviewedAt", "value": "2026-02-20T12:00:00Z"}, {"op": "add", "path": "/formData/reviewedBy", "value": AGENT_ID}, ]) ``` On the frontend, the `FormField` component from the [section above](#highlight-fields) will highlight each field as the agent focuses on it, and `useStorage` will reflect the patched values as they arrive in real time, no extra wiring required. ## Triggering agentic workflows [#triggers] There are several natural places to start an agent workflow. Choose the one that fits your product best, or combine multiple triggers.
AI agent mentioned in a comment
### Comments webhooks—mentioning an AI agent [#comments-webhook] Users can invoke an agent by mentioning it in a comment (e.g. `@AI Agent, please review this form`). The [`commentCreated`](/docs/platform/webhooks#CommentCreatedEvent) webhook fires for every new comment, giving you a place to detect the mention and dispatch the agent. Register the agent as a mentionable user Add the agent’s ID to [`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers) and [`resolveMentionSuggestions`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveMentionSuggestions) in your [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider) so it appears in the `@` mention picker alongside real users. ```tsx { const users = await fetchUsersFromDB(userIds); return userIds.map((id) => { if (id === "ai-agent") { return { name: "AI Agent", avatar: "/ai-agent-avatar.png" }; } return users.find((u) => u.id === id); }); }} resolveMentionSuggestions={async ({ text }) => { const userIds = await searchUsers(text); // Include the AI agent whenever "ai agent" matches the search text if ("ai agent".includes(text.toLowerCase())) { userIds.unshift("ai-agent"); } return userIds; }} > {/* ... */} ``` For more information on `resolveUsers` and `resolveMentionSuggestions`, see [Users and mentions](/docs/ready-made-features/comments/users-and-mentions). Handle the `commentCreated` webhook When a comment is created, the webhook payload contains `roomId`, `threadId`, and `commentId`. Fetch the full comment using the Liveblocks Node SDK, then use [`getMentionsFromCommentBody`](/docs/api-reference/liveblocks-node#getMentionsFromCommentBody) to extract all mentions and check whether the agent’s ID is among them. ```ts file="app/api/liveblocks-webhook/route.ts" import { Liveblocks, getMentionsFromCommentBody } from "@liveblocks/node"; const liveblocks = new Liveblocks({ secret: process.env.SECRET_KEY! }); export async function POST(req: Request) { const event = await req.json(); if (event.type === "commentCreated") { const { roomId, threadId, commentId } = event.data; // Fetch the full comment to inspect its body const comment = await liveblocks.getComment({ roomId, threadId, commentId, }); // Extract all mentions from the comment body const mentions = getMentionsFromCommentBody(comment.body); const mentionsAgent = mentions.some( (mention) => mention.kind === "user" && mention.id === "ai-agent" ); if (mentionsAgent) { // Trigger your agent workflow with the relevant context await triggerAgentWorkflow({ roomId, threadId, commentId }); } } return new Response(null, { status: 200 }); } ``` `triggerAgentWorkflow` is where you kick off your agent — call an n8n webhook, invoke a CrewAI crew, hit an LLM API, or run any other pipeline you've built. Always verify the webhook signature before processing events. See the [Webhooks documentation](/docs/platform/webhooks) for verification examples. ### Storage webhooks [#storage-webhook] The `storageUpdated` event fires whenever Storage is written to. This is useful for agents that react to user edits, for example auto-validation, auto-summarization, or data enrichment. ```ts file="app/api/liveblocks-webhook/route.ts" export async function POST(req: Request) { const event = await req.json(); if (event.type === "storageUpdated") { const { roomId } = event.data; await triggerAgentWorkflow({ roomId }); } return new Response(null, { status: 200 }); } ``` ### Direct API call or button [#direct-trigger] The simplest trigger of all: a user clicks a button in your UI, which calls your backend endpoint, which runs the agent. This gives you full control over when and how the agent is invoked. ```tsx async function handleReviewClick() { await fetch("/api/run-agent", { method: "POST", body: JSON.stringify({ roomId: "my-room" }), }); } ``` A scheduled job (CRON) is another variant which can run the agent periodically to batch process rooms or perform maintenance tasks. ### Other triggers - **Notification webhooks**: Fire when a user receives an inbox notification, which can be a signal to summarize unread activity or draft a reply. - **External events**: Slack messages, GitHub PR events, email, Zapier triggers, or any inbound webhook that carries a `roomId` can start an agent run. - **Scheduled / CRON jobs**: Poll rooms on a fixed schedule to run quality checks, generate reports, or clean up stale data. ## Integrating with agentic frameworks [#frameworks] Because both APIs are plain HTTP endpoints, Liveblocks works with any agent framework that can make HTTP requests. - **n8n**: Add an HTTP Request node to call the presence and JSON Patch endpoints. Use a Liveblocks webhook as the workflow trigger so agents fire automatically on comment or storage events. - **CrewAI** :Wrap the REST calls in a custom `Tool` and pass it to your agents. Agents can then set presence or patch storage as part of a multi-step task. - **LangChain / LangGraph**: Define Liveblocks tools for function-calling models. The LLM can generate JSON Patch operations directly — the RFC 6902 format is part of many models' training data, so no custom schema or special prompting is needed. - **Any HTTP-capable framework**: fetch, axios, Python `requests`, Go `net/http`, cURL—if it can make a `POST` or `PATCH` request it can drive Liveblocks. ## Next steps - [Modifying Storage via REST API with JSON Patch](/guides/modifying-storage-via-rest-api-with-json-patch): Full reference for all JSON Patch operations. - [Webhooks](/docs/platform/webhooks): All available webhook events and signature verification. - [REST API reference](/docs/api-reference/rest-api-endpoints): Complete request and response schemas for all endpoints. - [Users and mentions](/docs/ready-made-features/comments/users-and-mentions): Setting up `resolveUsers` and `resolveMentionSuggestions`. --- meta: title: "Fixing Next.js server component errors" description: "Learn how to fix Next.js server component problems" --- ## Fixing the warning: "Only plain objects can be passed to Client Components from Server Components" This error occurs when you create a Liveblocks data structure (`LiveObject`, `LiveList`, or `LiveMap`) within a server component. This is often occurs when creating a room with `RoomProvider`, for example: ```tsx file="layout.tsx" import { ReactNode } from "react"; import { RoomProvider } from "../liveblocks.config"; import { LiveObject } from "@liveblocks/client"; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ``` This can be fixed by turning the file into a client file with `"use client";` at the top of the file. However, if your need a server component, you can extract the provider to a different file. ## Structuring your app To take this further, and to learn how to structure your Next.js app in the best possible way, make sure to read our [How to use Liveblocks with Next.js /app directory](/docs/guides/how-to-use-liveblocks-with-nextjs-app-directory) guide. --- meta: title: "Getting ProseMirror state on the server" description: "Learn how to retrieve your document’s ProseMirror state on the server" --- Using [`@liveblocks/node-prosemirror`](/docs/api-reference/liveblocks-node-prosemirror), it’s possible to retrieve the state of your ProseMirror document on the server. ## Getting document state To get your document state, you can use [`withProsemirrorDocument`](/docs/api-reference/liveblocks-node-prosemirror#withProsemirrorDocument) and [`api.getText`](/docs/api-reference/liveblocks-node-prosemirror#api.getText). ```ts import { Liveblocks } from "@liveblocks/node"; import { withProsemirrorDocument } from "@liveblocks/node-prosemirror"; const liveblocks = new Liveblocks({ secret: "{{SECRET_KEY}}", }); // +++ const textContent = await withProsemirrorDocument( { roomId: "your-room-id", client: liveblocks, field: "prosemirror" }, async (api) => { return api.getText(); } ); // +++ // "My content" console.log(textContent); ``` ## Modifying document state To modify document state with transactions, use [`api.update`](/docs/api-reference/liveblocks-node-prosemirror#api.update). 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). ```ts 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, field: "prosemirror" }, // +++ async (api) => { await api.update((_, tr) => { // Transaction example return tr.insertText("Hello world"); }); } // +++ ); ``` ## Using Yjs APIs instead We don’t generally recommend it, but it’s also possible to use [`@liveblocks/node`](/docs/api-reference/liveblocks-node) to retrieve the state of your ProseMirror document, and its [`Y.Doc`](https://docs.yjs.dev/api/y.doc), on the server. This may give you more control in some cases. Using [`Liveblocks.getYjsDocumentAsBinaryUpdate`](/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc-binary) you can fetch your Yjs data, and place it inside a `Y.Doc`. We can then call `yDocToProseMirror` from [`y-prosemirror`](https://github.com/yjs/y-prosemirror) to retrieve the ProseMirror editor’s state. ```ts import * as Y from "yjs"; import { Liveblocks } from "@liveblocks/node"; import { yDocToProsemirrorJSON } from "y-prosemirror"; const liveblocks = new Liveblocks({ secret: "{{SECRET_KEY}}", }); export async function POST() { // Get your Yjs data as a binary update const update = await liveblocks.getYjsDocumentAsBinaryUpdate("my-room-name"); // Create a Yjs document const yDoc = new Y.Doc(); // Apply the binary update to `yDoc` Y.applyUpdate(yDoc, new Uint8Array(update)); // Get ProseMirror state from the default Yjs property it uses, "prosemirror" const prosemirrorState = yDocToProsemirrorJSON(yDoc, "prosemirror"); // { type: "doc", content: [{ type: "paragraph", content: [...] }] } console.log(prosemirrorState); } ``` If you’d like to edit your `Y.Doc`, make sure to read [how to use your `Y.Doc` on the server](/docs/guides/how-to-use-your-ydoc-on-the-server). --- meta: title: "Getting Tiptap state on the server" description: "Learn how to retrieve your document’s Tiptap state on the server" --- Using [`@liveblocks/node-prosemirror`](/docs/api-reference/liveblocks-node-prosemirror), it’s possible to retrieve the state of your Tiptap document on the server. Tiptap is not easy to edit on the server directly, which is why it’s necessary to use the ProseMirror package. Tiptap is an extension of ProseMirror. ## Getting document state To get your document state, you can use [`withProsemirrorDocument`](/docs/api-reference/liveblocks-node-prosemirror#withProsemirrorDocument) and [`api.getText`](/docs/api-reference/liveblocks-node-prosemirror#api.getText). ```ts import { Liveblocks } from "@liveblocks/node"; import { withProsemirrorDocument } from "@liveblocks/node-prosemirror"; const liveblocks = new Liveblocks({ secret: "{{SECRET_KEY}}", }); // +++ const textContent = await withProsemirrorDocument( { roomId: "your-room-id", client: liveblocks }, async (api) => { return api.getText(); } ); // +++ // "My content" console.log(textContent); ``` ## Modifying document state To modify document state with transactions, use [`api.update`](/docs/api-reference/liveblocks-node-prosemirror#api.update). 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). ```ts 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 }, // +++ async (api) => { await api.update((_, tr) => { // Transaction example return tr.insertText("Hello world"); }); } // +++ ); ``` You’ve now learned to fetch and modify document state! ## Using Yjs APIs instead We don’t generally recommend it, but it’s also possible to use [`@liveblocks/node`](/docs/api-reference/liveblocks-node) to retrieve the state of your Tiptap document, and its [`Y.Doc`](https://docs.yjs.dev/api/y.doc), on the server. This may give you more control in some cases. With [`Liveblocks.getYjsDocumentAsBinaryUpdate`](/docs/api-reference/liveblocks-node#get-rooms-roomId-ydoc-binary) you can fetch your Yjs data, and place it inside a `Y.Doc`. Because Tiptap is a wrapper around ProseMirror, we can then call `yDocToProseMirror` from [`y-prosemirror`](https://github.com/yjs/y-prosemirror) to retrieve the Tiptap editor’s state. ```ts import * as Y from "yjs"; import { Liveblocks } from "@liveblocks/node"; import { yDocToProsemirrorJSON } from "y-prosemirror"; const liveblocks = new Liveblocks({ secret: "{{SECRET_KEY}}", }); export async function POST() { // Get your Yjs data as a binary update const update = await liveblocks.getYjsDocumentAsBinaryUpdate("my-room-name"); // Create a Yjs document const yDoc = new Y.Doc(); // Apply the binary update to `yDoc` Y.applyUpdate(yDoc, new Uint8Array(update)); // Get Tiptap state from the Yjs property it uses, "default" const tiptapState = yDocToProsemirrorJSON(yDoc, "default"); // { type: "doc", content: [{ type: "paragraph", content: [...] }] } console.log(tiptapState); } ``` If you’d like to edit your `Y.Doc`, make sure to read [how to use your `Y.Doc` on the server](/docs/guides/how-to-use-your-ydoc-on-the-server). --- meta: title: "How to add users to Liveblocks Comments" description: "Learn how to add your user’s avatars and names to Liveblocks Comments using the resolver functions." --- After following the get started guide for Comments, you’ll notice that each user is currently “Anonymous”, and that there’s no way to mention or tag other users. To enable these features, we need to tell Comments where to find your users’ information.
Thread with resolved users
## What we’re learning In this guide we’ll be modifying [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), learning how to: - Add names and avatars to threads and comments with [`resolveUsers`](/docs/api-reference/liveblocks-react#resolveUsers). - Create user mention suggestions in the composer using [`resolveMentionSuggestions`](/docs/api-reference/liveblocks-react#resolveMentionSuggestions). ## Authenticate your application The first step is to find an [authentication guide for your framework](/docs/authentication) and authenticate your app, as this is necessary for Comments. Make sure to follow the metadata step in the guide, and attach the name of your user, along with the URL of their avatar, as these properties will both be used in the [default components](/docs/ready-made-features/comments#Components). Here’s an example using ID token authentication, our recommended method, with an email address as a user’s ID. ```ts title="Metadata in ID tokens" const { status, body } = await liveblocks.identifyUser( { userId: "marc@example.com", // Optional // organizationId: "org-id", // groupIds: ["group-id-1", "group-id-2"], }, { userInfo: { name: "Marc", avatar: "https://example.com/marc.png", // Your custom metadata // ... }, } ); ``` Modify your `UserMeta` type in `liveblocks.config.ts` to match the metadata format, adding type hints to your editor. ```ts file="liveblocks.config.ts" declare global { interface Liveblocks { UserMeta: { id: string; info: { name: string; avatar: string; // Your custom metadata // ... }; }; } } ``` ## Resolving users To show each user’s name and avatar in threads and comments, we need to use [`resolveUsers`](https://liveblocks.io/docs/api-reference/liveblocks-client#resolveUsers).
Thread with resolved users
Add the function to your LiveblocksProvider The [`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers) function is passed as an option to [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)—let’s add it. This function provides you with `userIds`, an array of user IDs that have interacted with Comments. These `userIds` match the IDs set when authenticating users in your app. ```tsx highlight="2-8" { // ["marc@example.com", ...] console.log(userIds); // Return a list of users // ... }} // ... />; ``` Return your users `resolveUsers` requires you to return a list of users in the `UserMeta["info"]` format we set earlier. Remember that _name_ and _avatar_ are required for the default components, but you can also use any other metadata in your app. ```tsx highlight="7-16" { // ["marc@example.com", ...] console.log(userIds); // Return a list of users return [ { name: "Marc", avatar: "https://example.com/marc.png", // Your custom metadata // ... }, // ... ]; }} // ... />; ``` We’re only returning one user here, but make sure to return an array containing each user, in the same order you received the IDs. Real-world example In your real application you’ll probably be getting users from your API endpoint and database via `fetch`. This is how we’d recommend building out this function. ```tsx highlight="3-7" { // Get users from your back end const users = await __fetchUsers__(userIds); // Return a list of users return users; }} // ... />; ``` Users are now visible After adding this, you should now be able to see your users in threads!
Thread with resolved users
## Resolving mention suggestions We can see the users that have commented, but we don’t have a way to search for users to mention, for example after typing the `@` character. We can create a simple search that resolves this data with [`resolveMentionSuggestions`](/docs/api-reference/liveblocks-react#resolveMentionSuggestions).
Working Comments mentions
Add the function to your config file `resolveMentionSuggestions` is placed alongside `resolveUsers`, and provides you with `text`, which is the string that the user is searching for. You can use this string to return a list of matching user IDs. ```tsx highlight="5-11" { // ... }} resolveMentionSuggestions={async ({ text, roomId }) => { // The text the user is searching for, e.g. "mar" console.log(text); // Return a list of user IDs that match the query return ["marc@example.com", "marissa@example.com"]; }} // ... />; ``` Real-world example In a real application, you’ll most likely be getting a list of each user, before filtering the list by the user’s names or IDs. If `text` is an empty string, then you need to return a list of every user, instead of a filtered list. ```tsx highlight="6-16" { // ... }} resolveMentionSuggestions={async ({ text, roomId }) => { // Fetch all users from your back end let users = await __fetchAllUsers__(); // If there's a query, filter for the relevant users if (text) { // Filter any way you'd like, e.g. checking if the name matches users = users.filter((user) => user.name.includes(text)); } // Return the filtered `userIds` return users.map((user) => user.id); }} // ... />; ``` Mention suggestions now appear Now we’ve found and returned the correct users, Comments can display a list of mention suggestions!
Working Comments mentions
## Next steps You’re now ready to start building your Comments application! Here’s where you can learn more: - [API reference](/docs/api-reference/liveblocks-react#Comments) - [Component reference](/docs/api-reference/liveblocks-react-ui#Comments) - [Examples](/examples/browse/comments) --- meta: title: "How to add users to Liveblocks Notifications" description: "Learn how to add your user’s avatars and names to Liveblocks Notifications using the resolver functions." --- After following the get started guide for Notifications, and sending notifications with [Comments](/docs/ready-made-features/comments) or [Text editor](/docs/ready-made-features/text-editor), you’ll notice that each user is currently “Anonymous”, and that there’s no way to mention or tag other users. To enable these features, we need to tell Notifications where to find your users’ information.
Thread
## What we’re learning In this guide we’ll be modifying [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), learning how to: - Add names and avatars to notifications with [`resolveUsers`](/docs/api-reference/liveblocks-react#resolveUsers). ## Authenticate your application The first step is to find an [authentication guide for your framework](/docs/authentication) and authenticate your app, as this is necessary for Notifications. Make sure to follow the metadata step in the guide, and attach the name of your user, along with the URL of their avatar, as these properties will both be used in the [default components](/docs/ready-made-features/notifications#Components). Here’s an example using ID token authentication, our recommended method, with an email address as a user’s ID. ```ts title="Metadata in ID tokens" const { status, body } = await liveblocks.identifyUser( { userId: "marc@example.com", // Optional // organizationId: "org-id", // groupIds: ["group-id-1", "group-id-2"], }, { userInfo: { name: "Marc", avatar: "https://example.com/marc.png", // Your custom metadata // ... }, } ); ``` Modify your `UserMeta` type in `liveblocks.config.ts` to match the metadata format, adding type hints to your editor. ```ts file="liveblocks.config.ts" declare global { interface Liveblocks { UserMeta: { id: string; info: { name: string; avatar: string; // Your custom metadata // ... }; }; } } ``` ## Resolving users To show each user’s name and avatar in threads and comments, we need to use [`resolveUsers`](https://liveblocks.io/docs/api-reference/liveblocks-client#resolveUsers).
Thread
Add the function to your LiveblocksProvider The [`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers) function is passed as an option to [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)—let’s add it. This function provides you with `userIds`, an array of user IDs that have interacted with Notifications. These `userIds` match the IDs set when authenticating users in your app. ```tsx highlight="2-8" { // ["marc@example.com", ...] console.log(userIds); // Return a list of users // ... }} // ... />; ``` Return your users `resolveUsers` requires you to return a list of users in the `UserMeta["info"]` format we set earlier. Remember that _name_ and _avatar_ are required for the default components, but you can also use any other metadata in your app. ```tsx highlight="7-16" { // ["marc@example.com", ...] console.log(userIds); // Return a list of users return [ { name: "Marc", avatar: "https://example.com/marc.png", // Your custom metadata // ... }, // ... ]; }} // ... />; ``` We’re only returning one user here, but make sure to return an array containing each user, in the same order you received the IDs. Real-world example In your real application you’ll probably be getting users from your API endpoint and database via `fetch`. This is how we’d recommend building out this function. ```tsx highlight="3-7" { // Get users from your back end const users = await __fetchUsers__(userIds); // Return a list of users return users; }} // ... />; ``` Users are now visible After adding this, you should now be able to see your users in notifications!
Thread
## Next steps You’re now ready to start building your Notifications application! Here’s where you can learn more: - [API reference](/docs/api-reference/liveblocks-react#Notifications) - [Component reference](/docs/api-reference/liveblocks-react-ui#Notifications) - [Examples](/examples/browse/notifications) --- meta: title: "How to add users to Liveblocks presence components" description: "Learn how to add your user’s avatars and names to Liveblocks avatar stack and cursors componentsusing the resolver functions." --- After following the get started guide for presence components, you’ll notice that each user is currently “Anonymous”, and that there’s no way to mention or tag other users. To enable these features, we need to tell Liveblocks where to find your users’ information.
Cursors with resolved users
## What we’re learning In this guide we’ll be modifying [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), learning how to: - Authenticate your application with [`authEndpoint`](/docs/api-reference/liveblocks-react#LiveblocksProviderAuthEndpoint) and [`identifyUser`](/docs/api-reference/liveblocks-node#id-tokens). - Add names and avatars to threads and comments with [`resolveUsers`](/docs/api-reference/liveblocks-react#resolveUsers). ## Authenticate your application The first step is to find an [authentication guide for your framework](/docs/authentication) and authenticate your app, as this is necessary for adding users. Here’s an example using ID token authentication, our recommended method, with an email address as a user’s ID. ```ts title="Metadata in ID tokens" const { status, body } = await liveblocks.identifyUser( { userId: "marc@example.com", // Optional // organizationId: "org-id", // groupIds: ["group-id-1", "group-id-2"], }, { userInfo: { // Optional, custom metadata // ... }, } ); ``` ## Resolving users To show each user’s name and avatar in the ready-made [`AvatarStack`](/docs/api-reference/liveblocks-react-ui#AvatarStack) and [`Cursors`](/docs/api-reference/liveblocks-react-ui#Cursors) components, we need to use [`resolveUsers`](https://liveblocks.io/docs/api-reference/liveblocks-client#resolveUsers).
Avatar stack with resolved users
Add the function to your LiveblocksProvider The [`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers) function is passed as an option to [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)—let’s add it. This function provides you with `userIds`, an array of user IDs that have interacted with Comments. These `userIds` match the IDs set when authenticating users in your app. ```tsx highlight="2-8" { // ["marc@example.com", ...] console.log(userIds); // Return a list of users // ... }} // ... />; ``` Return your users `resolveUsers` requires you to return a list of users in the `UserMeta["info"]` format we set earlier. Remember that _name_ and _avatar_ are required for the default components, but you can also use any other metadata in your app. ```tsx highlight="7-16" { // ["marc@example.com", ...] console.log(userIds); // Return a list of users return [ { name: "Marc", avatar: "https://example.com/marc.png", // Your custom metadata // ... }, // ... ]; }} // ... />; ``` We’re only returning one user here, but make sure to return an array containing each user, in the same order you received the IDs. Real-world example In your real application you’ll probably be getting users from your API endpoint and database via `fetch`. This is how we’d recommend building out this function. ```tsx highlight="3-7" { // Get users from your back end const users = await __fetchUsers__(userIds); // Return a list of users return users; }} // ... />; ``` Render the presence components Import the presence components in your app to see them. `Cursors` wraps around the component where the cursors will appear. ```tsx import { AvatarStack, Cursors } from "@liveblocks/react-ui"; function App() { return ( {/* Rest of your app here */} ); } ``` Users are now visible After adding this, you should now be able to see your users in presence components!
Avatar stack with resolved users
## Next steps You’re now ready to start building your multiplayer Liveblocks application! Here’s where you can learn more: - [Overview](/docs/ready-made-features/multiplayer) - [API reference](/docs/api-reference/liveblocks-react) - [Examples](/examples/browse/comments) --- meta: title: "How to add users to Liveblocks Text Editor" description: "Learn how to add your user’s avatars and names to Liveblocks Text Editor using the resolver functions." --- After following the get started guide for Text Editor, you’ll notice that each user is currently “Anonymous”, and that there’s no way to mention or tag other users. To enable these features, we need to tell Text Editor where to find your users’ information.
User mentions
## What we’re learning In this guide we’ll be modifying [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider), learning how to: - Add names and avatars to cursors and suggestions with [`resolveUsers`](/docs/api-reference/liveblocks-react#resolveUsers). - Create user mention suggestions in the editor using [`resolveMentionSuggestions`](/docs/api-reference/liveblocks-react#resolveMentionSuggestions). ## Authenticate your application The first step is to find an [authentication guide for your framework](/docs/authentication) and authenticate your app, as this is necessary for Text Editor. Make sure to follow the metadata step in the guide, and attach the name of your user, the color of their cursor, and their avatar URL, as these properties will both be used in the [Text editor](/docs/ready-made-features/text-editor). Here’s an example using ID token authentication, our recommended method, with an email address as a user’s ID. ```ts title="Metadata in ID tokens" const { status, body } = await liveblocks.identifyUser( { userId: "marc@example.com", // Optional // organizationId: "org-id", // groupIds: ["group-id-1", "group-id-2"], }, { userInfo: { name: "Marc", avatar: "https://example.com/marc.png", // Your custom metadata // ... }, } ); ``` Modify your `UserMeta` type in `liveblocks.config.ts` to match the metadata format, adding type hints to your editor. ```ts file="liveblocks.config.ts" declare global { interface Liveblocks { UserMeta: { id: string; info: { name: string; avatar: string; // Your custom metadata // ... }; }; } } ``` ## Resolving users To show each user’s name and color in their cursors, we need to use [`resolveUsers`](https://liveblocks.io/docs/api-reference/liveblocks-client#resolveUsers).
Real-time text cursors
Add the function to your LiveblocksProvider The [`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers) function is passed as an option to [`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)—let’s add it. This function provides you with `userIds`, an array of user IDs that have interacted with Text Editor. These `userIds` match the IDs set when authenticating users in your app. ```tsx highlight="2-8" { // ["marc@example.com", ...] console.log(userIds); // Return a list of users // ... }} // ... />; ``` Return your users `resolveUsers` requires you to return a list of users in the `UserMeta["info"]` format we set earlier. Remember that _name_, _color_, and _avatar_, are required for the editor component, but you can also use any other metadata in your app. ```tsx highlight="7-17" { // ["marc@example.com", ...] console.log(userIds); // Return a list of users return [ { name: "Marc", color: "#00ff00", avatar: "https://example.com/marc.png", // Your custom metadata // ... }, // ... ]; }} // ... />; ``` We’re only returning one user here, but make sure to return an array containing each user, in the same order you received the IDs. Real-world example In your real application you’ll probably be getting users from your API endpoint and database via `fetch`. This is how we’d recommend building out this function. ```tsx highlight="3-7" { // Get users from your back end const users = await __fetchUsers__(userIds); // Return a list of users return users; }} // ... />; ``` Users are now visible After adding this, you should now be able to see your user names in cursors!
Real-time text cursors
## Resolving mention suggestions We can see the users that are connected, but we don’t have a way to search for users to mention inline, for example after typing the `@` character. We can create a simple search that resolves this data with [`resolveMentionSuggestions`](/docs/api-reference/liveblocks-react#resolveMentionSuggestions).
User mentions
Add the function to your config file `resolveMentionSuggestions` is placed alongside `resolveUsers`, and provides you with `text`, which is the string that the user is searching for. You can use this string to return a list of matching user IDs. ```tsx highlight="5-11" { // ... }} resolveMentionSuggestions={async ({ text, roomId }) => { // The text the user is searching for, e.g. "mar" console.log(text); // Return a list of user IDs that match the query return ["marc@example.com", "marissa@example.com"]; }} // ... />; ``` Real-world example In a real application, you’ll most likely be getting a list of each user, before filtering the list by the user’s names or IDs. If `text` is an empty string, then you need to return a list of every user, instead of a filtered list. ```tsx highlight="6-16" { // ... }} resolveMentionSuggestions={async ({ text, roomId }) => { // Fetch all users from your back end let users = await __fetchAllUsers__(); // If there's a query, filter for the relevant users if (text) { // Filter any way you'd like, e.g. checking if the name matches users = users.filter((user) => user.name.includes(text)); } // Return the filtered `userIds` return users.map((user) => user.id); }} // ... />; ``` Mention suggestions now appear Now we’ve found and returned the correct users, Text Editor can display a list of mention suggestions!
User mentions
## Next steps You’re now ready to start building your Text Editor application! Here’s where you can learn more: - [API reference for Lexical React](/docs/api-reference/liveblocks-react-lexical) - [API reference for Lexical Node](/docs/api-reference/liveblocks-node-lexical) - [Examples](/examples/browse/text-editor) - [Overview](/docs/ready-made-features/text-editor) --- meta: title: "How to create a collaborative code editor with CodeMirror, Yjs, Next.js, and Liveblocks" description: "Build a collaborative code editor with CodeMirror, Yjs, Next.js, and Liveblocks" --- In this tutorial, we’ll be building a collaborative code editor using CodeMirror, Yjs, Next.js, and Liveblocks.
This guide assumes that you’re already familiar with [React](https://react.dev/), [Next.js](https://nextjs.org/), [TypeScript](https://www.typescriptlang.org/), and [CodeMirror](https://codemirror.net/). ## Install CodeMirror, Yjs, and Liveblocks into your Next.js application On GitHub we have a working example of this [collaborative CodeMirror editor](https://github.com/liveblocks/liveblocks/tree/main/examples/nextjs-yjs-codemirror) for you to download and run. Run the following command to install the CodeMirror, Yjs, and Liveblocks packages: ```bash npm install @liveblocks/client @liveblocks/react @liveblocks/node @liveblocks/yjs yjs codemirror @codemirror/lang-javascript y-codemirror.next ``` ## Set up access token authentication The first step in connecting to Liveblocks is to set up an authentication endpoint in `/app/api/liveblocks-auth/route.ts`. ```ts import { Liveblocks } from "@liveblocks/node"; import { NextRequest } from "next/server"; const API_KEY = "{{SECRET_KEY}}"; const liveblocks = new Liveblocks({ secret: API_KEY!, }); export async function POST(request: NextRequest) { // Get the current user's info from your database const user = { id: "charlielayne@example.com", info: { name: "Charlie Layne", color: "#D583F0", picture: "https://liveblocks.io/avatars/avatar-1.png", }, }; // Create a session for the current user // userInfo is made available in Liveblocks presence hooks, e.g. useOthers const session = liveblocks.prepareSession(user.id, { userInfo: user.info, }); // Give the user access to the room const { room } = await request.json(); session.allow(room, session.FULL_ACCESS); // Authorize the user and return the result const { body, status } = await session.authorize(); return new Response(body, { status }); } ``` Here’s an example using the older API routes format in `/pages`. ```ts file="pages/api/liveblocks-auth.ts" isCollapsed isCollapsable import { Liveblocks } from "@liveblocks/node"; import type { NextApiRequest, NextApiResponse } from "next"; const API_KEY = "{{SECRET_KEY}}"; const liveblocks = new Liveblocks({ secret: API_KEY!, }); export default async function handler( request: NextApiRequest, response: NextApiResponse ) { // Get the current user's info from your database const user = { id: "charlielayne@example.com", info: { name: "Charlie Layne", color: "#D583F0", picture: "https://liveblocks.io/avatars/avatar-1.png", }, }; // Create a session for the current user // userInfo is made available in Liveblocks presence hooks, e.g. useOthers const session = liveblocks.prepareSession(user.id, { userInfo: user.info, }); // Give the user access to the room const { room } = request.body; session.allow(room, session.FULL_ACCESS); // Authorize the user and return the result const { status, body } = await session.authorize(); response.status(status).send(body); } ``` ## Initialize your Liveblocks config file Let’s initialize the `liveblocks.config.ts` file in which you’ll set up the Liveblocks client. ```bash npx create-liveblocks-app@latest --init --framework react ``` We’ll also need another type for this tutorial. After creating the config file, open it up and insert the following: ```tsx file="liveblocks.config.ts" import { LiveblocksYjsProvider } from "@liveblocks/yjs"; // ... export type TypedLiveblocksProvider = LiveblocksYjsProvider< Presence, Storage, UserMeta, RoomEvent >; ``` ## 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. Create a file in the current directory within `/app`, and name it `Room.tsx`. Pass the location of your endpoint to `LiveblocksProvider`. ```tsx file="/app/Room.tsx" "use client"; import { ReactNode } from "react"; import { LiveblocksProvider, RoomProvider, ClientSideSuspense, } from "@liveblocks/react/suspense"; export function Room({ children }: { children: ReactNode }) { return ( Loading…}> {children} ); } ``` ## Set up the CodeMirror editor Now that we’ve set up Liveblocks, we can start integrating Monaco and Yjs in the `Editor.tsx` file. ```tsx file="Editor.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 { LiveblocksYjsProvider } from "@liveblocks/yjs"; import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config"; import styles from "./CollaborativeEditor.module.css"; // Collaborative code editor with undo/redo, live cursors, and live avatars export function CollaborativeEditor() { const room = useRoom(); const [element, setElement] = useState(); const [yUndoManager, setYUndoManager] = useState(); const ref = useCallback((node: HTMLElement | null) => { if (!node) return; setElement(node); }, []); // Set up Liveblocks Yjs provider and attach CodeMirror editor useEffect(() => { let provider: TypedLiveblocksProvider; let ydoc: Y.Doc; let view: EditorView; if (!element || !room || !userInfo) { return; } // Create Yjs provider and document ydoc = new Y.Doc(); provider = new LiveblocksYjsProvider(room as any, ydoc); const ytext = ydoc.getText("codemirror"); const undoManager = new Y.UndoManager(ytext); setYUndoManager(undoManager); // Set up CodeMirror and extensions const state = EditorState.create({ doc: ytext.toString(), extensions: [ basicSetup, javascript(), yCollab(ytext, provider.awareness, { undoManager }), ], }); // Attach CodeMirror to element view = new EditorView({ state, parent: element, }); return () => { ydoc?.destroy(); provider?.destroy(); view?.destroy(); }; }, [element, room, userInfo]); return (
); } ``` And here is the `Editor.module.css` file to make sure your multiplayer text editor looks nice and tidy. ```css file="Editor.module.css" isCollapsed isCollapsable .container { display: flex; flex-direction: column; position: relative; border-radius: 12px; background: #fff; width: 100%; height: 100%; color: #111827; overflow: hidden; } .editorHeader { display: flex; justify-content: space-between; align-items: center; } .editorContainer { position: relative; flex-grow: 1; overflow: auto; } ``` ## Add your editor to the current page Next, add the `CollaborativeEditor` into the page file, and place it inside the `Room` component we created earlier. We should now be seeing a basic collaborative editor! ```tsx file="/app/page.tsx" import { Room } from "./Room"; import CollaborativeEditor from "@/components/Editor"; export default function Page() { return ( ); } ``` ## Add live cursors To add live cursors to the code editor, we can get the `userInfo` for the current user with [`useSelf`](/docs/api-reference/liveblocks-react#useSelf), and attach it Yjs awareness. After adding the following, you should see live cursors: ```tsx file="Cursors.tsx" highlight="1,10-11,35-40" import { useRoom, useSelf } from "@/liveblocks.config"; // ... // Collaborative code editor with undo/redo, live cursors, and live avatars export function CollaborativeEditor() { const room = useRoom(); const [element, setElement] = useState(); const [yUndoManager, setYUndoManager] = useState(); // Get user info from Liveblocks authentication endpoint const userInfo = useSelf((me) => me.info); const ref = useCallback((node: HTMLElement | null) => { if (!node) return; setElement(node); }, []); // Set up Liveblocks Yjs provider and attach CodeMirror editor useEffect(() => { let provider: TypedLiveblocksProvider; let ydoc: Y.Doc; let view: EditorView; if (!element || !room || !userInfo) { return; } // Create Yjs provider and document ydoc = new Y.Doc(); provider = new LiveblocksYjsProvider(room as any, ydoc); const ytext = ydoc.getText("codemirror"); const undoManager = new Y.UndoManager(ytext); setYUndoManager(undoManager); // Attach user info to Yjs provider.awareness.setLocalStateField("user", { name: userInfo.name, color: userInfo.color, colorLight: userInfo.color + "80", // 6-digit hex code at 50% opacity }); // Set up CodeMirror and extensions const state = EditorState.create({ doc: ytext.toString(), extensions: [ basicSetup, javascript(), yCollab(ytext, provider.awareness, { undoManager }), ], }); // Attach CodeMirror to element view = new EditorView({ state, parent: element, }); return () => { ydoc?.destroy(); provider?.destroy(); view?.destroy(); }; }, [element, room, userInfo]); return (
); } ``` We can style these cursors by placing CSS in a global CSS file. ```css file="globals.css" isCollapsed isCollapsable /* Cursor name */ .cm-editor .cm-ySelectionInfo { position: absolute; top: -1.6em; left: -1px; padding: 2px 6px; opacity: 1; color: #fff; border: 0; border-radius: 6px; border-bottom-left-radius: 0; line-height: normal; white-space: nowrap; font-size: 14px; font-family: sans-serif; font-style: normal; font-weight: 600; pointer-events: none; user-select: none; z-index: 1000; } .cm-editor .cm-ySelectionCaretDot { display: none; } /* Other CodeMirror styles */ .cm-editor { height: 100%; font-size: 14px; } .cm-editor.cm-focused { outline: none; } .cm-editor .cm-scroller { padding-top: 1rem; } .cm-editor .cm-gutters { background: none; border: 0; } .cm-editor .cm-lineNumbers .cm-gutterElement { padding-left: 1rem; padding-right: 0.5rem; } ``` ## Add a toolbar From this point onwards, you can build your CodeMirror app as normal! For example, should you wish to add a basic undo/redo toolbar to your app: ```tsx file="Toolbar.tsx" import * as Y from "yjs"; import styles from "./Toolbar.module.css"; type Props = { yUndoManager: Y.UndoManager; }; export function Toolbar({ yUndoManager }: Props) { return (
); } ``` Add some matching styles: ```css file="Toolbar.module.css" isCollapsed isCollapsable .toolbar { display: flex; padding: 1em; gap: 6px; } .button { display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 6px; height: 32px; width: 32px; background: #fff; color: #1f2937; border: none; box-shadow: rgba(0, 0, 0, 0.12) 0 4px 8px 0, rgba(0, 0, 0, 0.02) 0 0 0 1px; } .button:hover { color: #111827; box-shadow: rgba(0, 0, 0, 0.16) 0 5px 8px 0, rgba(0, 0, 0, 0.04) 0 0 0 1px; } .button:focus-visible { outline-offset: 2px; } ``` You can then import this into your editor to enable basic CodeMirror features: ```tsx file="Editor.tsx" highlight="1,9-13" import { Toolbar } from "@/components/Toolbar"; // ... export function CollaborativeEditor() { // ... return (
{yUndoManager ? : null}
); } ``` ## Create live avatars with Liveblocks hooks Along with building out your code editor, you can now use other Liveblocks features, such as [Presence](/docs/ready-made-features/presence). The [`useOthers`](/docs/api-reference/liveblocks-react#useOthers) hook allows us to view information about each user currently online, and we can turn this into a live avatars component. ```tsx file="Avatars.tsx" import { useOthers, useSelf } from "@/liveblocks.config"; import styles from "./Avatars.module.css"; export function Avatars() { const users = useOthers(); const currentUser = useSelf(); return (
{users.map(({ connectionId, info }) => { return ( ); })} {currentUser && (
)}
); } export function Avatar({ picture, name }: { picture: string; name: string }) { return (
); } ``` And here’s the styles: ```css file="Avatars.module.css" isCollapsed isCollapsable .avatars { display: flex; padding: 0 0.75rem; } .avatar { display: flex; place-content: center; position: relative; border: 4px solid #fff; border-radius: 9999px; width: 42px; height: 42px; background-color: #9ca3af; margin-left: -0.75rem; } .avatar:before { content: attr(data-tooltip); position: absolute; top: 100%; opacity: 0; transition: opacity 0.15s ease; padding: 5px 10px; color: white; font-size: 0.75rem; border-radius: 8px; margin-top: 10px; z-index: 1; background: black; white-space: nowrap; } .avatar:hover:before { opacity: 1; } .avatar_picture { width: 100%; height: 100%; border-radius: 9999px; } ``` You can then import this to your editor to see it in action: ```tsx file="Editor.tsx" highlight="1,13" import { Avatars } from "@/components/Avatars"; // ... export function CollaborativeEditor() { // ... return (
{yUndoManager ? : null}
); } ``` Note that the cursors and avatars match in color and name, as the info for both is sourced from the Liveblocks authentication endpoint. ## Try it out You should now see the complete editor, along with live cursors, live avatars, and some basic features! On GitHub we have a working example of this [multiplayer code editor](https://github.com/liveblocks/liveblocks/tree/main/examples/nextjs-yjs-codemirror).
--- meta: title: "How to create a collaborative code editor with Monaco, Yjs, Next.js, and Liveblocks" description: "Build a collaborative code editor with Monaco, Yjs, Next.js, and Liveblocks" --- In this tutorial, we’ll be building a collaborative code editor using Monaco, Yjs, Next.js, and Liveblocks.
This guide assumes that you’re already familiar with [React](https://react.dev/), [Next.js](https://nextjs.org/), [TypeScript](https://www.typescriptlang.org/), and [Monaco](https://microsoft.github.io/monaco-editor/). ## Install Monaco, Yjs, and Liveblocks into your Next.js application On GitHub we have a working example of this [collaborative Monaco editor](https://github.com/liveblocks/liveblocks/tree/main/examples/nextjs-yjs-monaco) for you to download and run. Run the following command to install the Monaco, Yjs, and Liveblocks packages: ```bash npm install @liveblocks/client @liveblocks/react @liveblocks/yjs yjs @monaco-editor/react y-monaco y-protocols ``` ## Set up access token authentication The first step in connecting to Liveblocks is to set up an authentication endpoint in `/app/api/liveblocks-auth/route.ts`. ```ts import { Liveblocks } from "@liveblocks/node"; import { NextRequest } from "next/server"; const API_KEY = "{{SECRET_KEY}}"; const liveblocks = new Liveblocks({ secret: API_KEY!, }); export async function POST(request: NextRequest) { // Get the current user's info from your database const user = { id: "charlielayne@example.com", info: { name: "Charlie Layne", color: "#D583F0", picture: "https://liveblocks.io/avatars/avatar-1.png", }, }; // Create a session for the current user // userInfo is made available in Liveblocks presence hooks, e.g. useOthers const session = liveblocks.prepareSession(user.id, { userInfo: user.info, }); // Give the user access to the room const { room } = await request.json(); session.allow(room, session.FULL_ACCESS); // Authorize the user and return the result const { body, status } = await session.authorize(); return new Response(body, { status }); } ``` Here’s an example using the older API routes format in `/pages`. ```ts file="pages/api/liveblocks-auth.ts" isCollapsed isCollapsable import { Liveblocks } from "@liveblocks/node"; import type { NextApiRequest, NextApiResponse } from "next"; const API_KEY = "{{SECRET_KEY}}"; const liveblocks = new Liveblocks({ secret: API_KEY!, }); export default async function handler( request: NextApiRequest, response: NextApiResponse ) { // Get the current user's info from your database const user = { id: "charlielayne@example.com", info: { name: "Charlie Layne", color: "#D583F0", picture: "https://liveblocks.io/avatars/avatar-1.png", }, }; // Create a session for the current user // userInfo is made available in Liveblocks presence hooks, e.g. useOthers const session = liveblocks.prepareSession(user.id, { userInfo: user.info, }); // Give the user access to the room const { room } = request.body; session.allow(room, session.FULL_ACCESS); // Authorize the user and return the result const { status, body } = await session.authorize(); response.status(status).send(body); } ``` ## Initialize your Liveblocks config file Let’s initialize the `liveblocks.config.ts` file in which you’ll set up the Liveblocks client. ```bash npx create-liveblocks-app@latest --init --framework react ``` We’ll also need another type for this tutorial. After creating the config file, open it up and insert the following: ```tsx file="liveblocks.config.ts" import { LiveblocksYjsProvider } from "@liveblocks/yjs"; // ... export type TypedLiveblocksProvider = LiveblocksYjsProvider< Presence, Storage, UserMeta, RoomEvent >; ``` ## 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. Create a file in the current directory within `/app`, and name it `Room.tsx`. Pass the location of your endpoint to `LiveblocksProvider`. ```tsx file="/app/Room.tsx" "use client"; import { ReactNode } from "react"; import { LiveblocksProvider, RoomProvider, ClientSideSuspense, } from "@liveblocks/react/suspense"; export function Room({ children }: { children: ReactNode }) { return ( Loading…}> {children} ); } ``` ## Set up the Monaco editor Now that we’ve set up Liveblocks, we can start integrating Monaco and Yjs in the `Editor.tsx` file. ```tsx file="Editor.tsx" "use client"; import * as Y from "yjs"; import { LiveblocksYjsProvider } from "@liveblocks/yjs"; import { TypedLiveblocksProvider, useRoom } from "@/liveblocks.config"; import { useCallback, useEffect, useState } from "react"; import styles from "./CollaborativeEditor.module.css"; import { Editor } from "@monaco-editor/react"; import { editor } from "monaco-editor"; import { MonacoBinding } from "y-monaco"; import { Awareness } from "y-protocols/awareness"; // Collaborative code editor with undo/redo, live cursors, and live avatars export function CollaborativeEditor() { const room = useRoom(); const [provider, setProvider] = useState(); const [editorRef, setEditorRef] = useState(); // Set up Liveblocks Yjs provider and attach Monaco editor useEffect(() => { let yProvider: TypedLiveblocksProvider; let yDoc: Y.Doc; let binding: MonacoBinding; if (editorRef) { yDoc = new Y.Doc(); const yText = yDoc.getText("monaco"); yProvider = new LiveblocksYjsProvider(room, yDoc); setProvider(yProvider); // Attach Yjs to Monaco binding = new MonacoBinding( yText, editorRef.getModel() as editor.ITextModel, new Set([editorRef]), yProvider.awareness as Awareness ); } return () => { yDoc?.destroy(); yProvider?.destroy(); binding?.destroy(); }; }, [editorRef, room]); const handleOnMount = useCallback((e: editor.IStandaloneCodeEditor) => { setEditorRef(e); }, []); return (
); } ``` And here is the `Editor.module.css` file to make sure your multiplayer text editor looks nice and tidy. ```css file="Editor.module.css" isCollapsed isCollapsable .container { display: flex; flex-direction: column; position: relative; border-radius: 12px; background: #fff; width: 100%; height: 100%; color: #111827; overflow: hidden; } .editorHeader { display: flex; justify-content: space-between; align-items: center; } .editorContainer { position: relative; flex-grow: 1; } ``` ## Add your editor to the current page Next, add the `CollaborativeEditor` into the page file, and place it inside the `Room` component we created earlier. We should now be seeing a basic collaborative editor! ```tsx file="/app/page.tsx" import { Room } from "./Room"; import CollaborativeEditor from "@/components/Editor"; export default function Page() { return ( ); } ``` ## Add live cursors To add live cursors to the code editor, we can get the `userInfo` for the current user with [`useSelf`](/docs/api-reference/liveblocks-react#useSelf), and attach it Yjs awareness. Currently, the only way to style this is to loop through each Yjs user, and dynamically insert CSS styles into the page, using `::after` to display users’ names. We’ll place this in a new file: ```tsx file="Cursors.tsx" import { useEffect, useMemo, useState } from "react"; import { AwarenessList, TypedLiveblocksProvider, UserAwareness, useSelf, } from "@/liveblocks.config"; type Props = { yProvider: TypedLiveblocksProvider; }; export function Cursors({ yProvider }: Props) { // Get user info from Liveblocks authentication endpoint const userInfo = useSelf((me) => me.info); const [awarenessUsers, setAwarenessUsers] = useState([]); useEffect(() => { // Add user info to Yjs awareness const localUser: UserAwareness["user"] = userInfo; yProvider.awareness.setLocalStateField("user", localUser); // On changes, update `awarenessUsers` function setUsers() { setAwarenessUsers([...yProvider.awareness.getStates()] as AwarenessList); } yProvider.awareness.on("change", setUsers); setUsers(); return () => { yProvider.awareness.off("change", setUsers); }; }, [yProvider]); // Insert awareness info into cursors with styles const styleSheet = useMemo(() => { let cursorStyles = ""; for (const [clientId, client] of awarenessUsers) { if (client?.user) { cursorStyles += ` .yRemoteSelection-${clientId}, .yRemoteSelectionHead-${clientId} { --user-color: ${client.user.color}; } .yRemoteSelectionHead-${clientId}::after { content: "${client.user.name}"; } `; } } return { __html: cursorStyles }; }, [awarenessUsers]); return