---
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 signing 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 signing 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" && (
{args.origin} → {args.destination} on {args.departureDate}
{args.passengers.length} passenger(s)
)}
{stage === "executed" && result.data && (
Flight booked successfully!
Booking ID: {result.data.bookingId}
)}
);
},
});
```
In this example, the tool arguments stream in progressively during the
`receiving` stage, causing multiple re-renders as each field appears:
- **1st render**: `{ stage: "receiving", partialArgs: {} }`
- **2nd render**: `{ stage: "receiving", partialArgs: { origin: "New York" } }`
- **3rd render**:
`{ stage: "receiving", partialArgs: { origin: "New York", destination: "London" } }`
- **4th render**:
`{ stage: "receiving", partialArgs: { origin: "New York", destination: "London", departureDate: "2024-12-15" } }`
- **5th render**: `{ stage: "receiving", partialArgs: { ..., passengers: [] } }`
- **6th render**:
`{ stage: "receiving", partialArgs: { ..., passengers: [{ name: "John" }] } }`
- **7th render**:
`{ stage: "receiving", partialArgs: { ..., passengers: [{ name: "John", age: 3 }] } }`
- **8th render**:
`{ stage: "receiving", partialArgs: { ..., passengers: [{ name: "John", age: 30 }] } }`
- **Final render**: `{ stage: "executing", args: { /* complete object */ } }`
This demonstrates how each field and nested property appears incrementally,
providing real-time feedback to users as the AI constructs the tool call
arguments.
Arguments are streamed in forward-only order. Once a field begins appearing, all
previous fields are complete and won’t be modified. You’ll never see
`{ origin: "New York", destination: "London" }` followed by
`{ origin: "San Francisco", destination: "London" }`, but you might see
`{ origin: "New" }` then `{ origin: "New York" }` then
`{ origin: "New York", destination: "London" }`.
## Room
Room returned by [`client.enterRoom`][] (or [`client.getRoom`][]).
### Room.getPresence
Return the current user’s Presence.
[Presence](/docs/ready-made-features/presence) is used to store custom
properties on each user that exist until the user disconnects. An example use
would be storing a user’s cursor coordinates.
```ts
const presence = room.getPresence();
// { cursor: { x: 363, y: 723 } }
console.log(presence);
```
Presence is set with [`updatePresence`](#Room.updatePresence) and can be typed
when you [enter a room](#enter-room-typing-a-room). The example above is using
the following type:
```ts file="liveblocks.config.ts"
declare global {
interface Liveblocks {
Presence: {
cursor: { x: number; y: number };
};
}
}
```
An object holding the Presence value for the currently connected user.
Presence is set with [`updatePresence`](#Room.updatePresence). Will always
be JSON-serializable. `TPresence` is the `Presence` type you set yourself,
[learn more](#Typing-presence).
_None_
### Room.updatePresence
Updates the current user’s [Presence](/docs/ready-made-features/presence). Only
pass the properties you wish to update—any changes will be merged into the
current presence. The entire presence object will not be replaced.
```ts
room.updatePresence({ typing: true });
room.updatePresence({ status: "Online" });
// { typing: true, status: "Online" }
const presence = room.getPresence();
```
_Nothing_
The updated Presence properties for the current user inside an object. The
user’s entire Presence object will not be replaced, instead these properties
will be merged with the existing Presence. This object must be
JSON-serializable.
Adds Presence values to the history stack, meaning using undo and redo
functions will change them. [Learn more](#add-presence-to-history).
#### Add Presence to history [#add-presence-to-history]
By default, Presence values are not added to history. However, using the
`addToHistory` option will add items to the undo/redo stack.
```ts
room.updatePresence({ color: "blue" }, { addToHistory: true });
room.updatePresence({ color: "red" }, { addToHistory: true });
room.history.undo();
// { color: "blue" }
const presence = room.getPresence();
```
See [`room.history`][] for more information.
### Room.getOthers
Returns an array of currently connected users in the room. Returns a
[`User`](#user-type) object for each user. Note that you can also subscribe to
others using [`Room.subscribe("others")`](#Room.subscribe.others).
```ts
const others = room.getOthers();
for (const other of others) {
const { connectionId, id, info, presence, canWrite, canComment } = other;
// Do things
}
```
An array holding each connected user’s [`User`](#user-type) object. `User`
contains the current user’s Presence value, along with other information.
Presence is set with [`updatePresence`](#Room.updatePresence). Returns an
empty array when no other users are currently connected. Will always be
JSON-serializable.
_None_
### Room.broadcastEvent
Broadcast an event to other users in the Room. Events broadcast to the room can
be listened to with [`Room.subscribe("event")`][]. Takes a custom event payload
as first argument. Should be serializable to JSON.
```ts
room.broadcastEvent({ type: "REACTION", emoji: "🔥" });
```
_Nothing_
The event to broadcast to every other user in the room. Must be
JSON-serializable. `TRoomEvent` is the `RoomEvent` type you set yourself,
[learn more](#typing-multiple-events).
Queue the event if the connection is currently closed, or has not been
opened yet. We’re not sure if we want to support this option in the future
so it might be deprecated to be replaced by something else. [Learn
more](#broadcasting-an-event-when-disconnected).
#### Receiving an event
To receive an event, use [`Room.subscribe("event")`][]. The `user` property
received on the other end is the sender’s [`User`](#user-type) instance.
```ts
// User 1
room.broadcastEvent({ type: "REACTION", emoji: "🔥" });
// User 2
const unsubscribe = room.subscribe("event", ({ event, user, connectionId }) => {
// ^^^^ User 1
if (event.type === "REACTION") {
// Do something
}
});
```
We recommend using a property such as `type`, so that it’s easy to distinguish
between different events on the receiving end.
#### Typing multiple events [#typing-multiple-events]
When [defining your types](#typing-your-data), you can pass a `RoomEvent` type
in your config file to receive type hints in your app. To define multiple
different custom events, use a union.
```ts
declare global {
interface Liveblocks {
RoomEvent:
| { type: "REACTION"; emoji: string }
| { type: "ACTION"; action: string };
}
}
```
```ts
room.subscribe("event", ({ event, user, connectionId }) => {
if (event.type === "REACTION") {
// Do something
}
if (event.type === "ACTION") {
// Do something else
}
});
```
#### Broadcasting an event when disconnected [#broadcasting-an-event-when-disconnected]
By default, broadcasting an event is a “fire and forget” action. If the sending
client is not currently connected to a room, the event is simply discarded. When
passing the `shouldQueueEventIfNotReady` option, the client will queue up the
event, and only send it once the connection to the room is (re)established.
We’re not sure if we want to support `shouldQueueEventIfNotReady` in the future,
so it may be deprecated and replaced with something else.
```ts
room.broadcastEvent(
{ type: "REACTION", emoji: "🔥" },
{
// +++
shouldQueueEventIfNotReady: true,
// +++
}
);
```
### Room.getSelf
Gets the current [`User`](#user-type). Returns `null` if the client is not yet
connected to the room.
```ts
const { connectionId, presence, id, info, canWrite, canComment } =
room.getSelf();
```
Returns the current [`User`](#user-type). Returns `null` if the client is
not yet connected to the room.
_None_
Here’s an example of a full return value, assuming `Presence` and `UserMeta`
[have been set](#user-type).
```ts
const user = room.getSelf();
// {
// connectionId: 52,
// presence: {
// cursor: { x: 263, y: 786 },
// },
// id: "mislav.abha@example.com",
// info: {
// avatar: "/mislav.png",
// },
// canWrite: true,
// canComment: true,
// }
console.log(user);
```
### Room.getStatus
Gets the current WebSocket connection status of the room. The possible value
are: `initial`, `connecting`, `connected`, `reconnecting`, or `disconnected`.
```ts
const status = room.getStatus();
// "connected"
console.log(status);
```
Returns the room’s current connection status. It can return one of five values:
- `"initial"` The room has not attempted to connect yet.
- `"connecting"` The room is currently authenticating or connecting.
- `"connected"` The room is connected.
- `"reconnecting"` The room has disconnected, and is trying to connect again.
- `"disconnected"` The room is disconnected, and is no longer attempting to connect.
_None_
### Room.getStorageStatus
Get the Storage status. Use this to tell whether Storage has been synchronized
with the Liveblocks servers.
```ts
const status = room.getStorageStatus();
// "synchronizing"
console.log(status);
```
The current room’s Storage status. `status` can be one of four types.
- `"not-loaded"` Storage has not been loaded yet as [`room.getStorage`][] has not been called.
- `"loading"` Storage is currently loading for the first time.
- `"synchronizing"` Local Storage changes are currently being synchronized.
- `"synchronized"` Local Storage changes have been synchronized.
_None_
### Room.subscribe(storageItem)
Subscribe to updates on a particular storage item, and takes a callback function
that’s called when the storage item is updated. The Storage `root` is a
[`LiveObject`][], which means you can subscribe to this, as well as other live
structures. Returns an unsubscribe function.
```ts
const { root } = await room.getStorage();
const unsubscribe = room.subscribe(root, (updatedRoot) => {
// Do something
});
```
Unsubscribe function. Call it to cancel the subscription.
The `LiveObject`, `LiveMap`, or `LiveList` which is being subscribed to.
Each time the structure is updated, the callback is called.
Function that’s called when `storageItem` updates. Returns the updated
storage structure.
Subscribe to both `storageItem` and its children. The callback function will
be passed a list of updates instead of just the new Storage item. [Learn
more](#listening-for-nested-changes).
#### Typing Storage
To type the Storage values you receive, make sure to set your `Storage` type.
```ts file="liveblocks.config.ts"
import { LiveList } from "@liveblocks/client";
declare global {
interface Liveblocks {
Storage: {
animals: LiveList<{ name: string }>;
};
}
}
```
The type received in the callback will match the type passed. Learn more under
[typing your room](#typing-your-data).
```ts
const { root } = await room.getStorage();
const animals = root.get("animals");
const unsubscribe = room.subscribe(animals, (updatedAnimals) => {
// LiveList<[{ name: "Fido" }, { name: "Felix" }]>
console.log(updatedAnimals);
});
```
#### Subscribe to any live structure
You can subscribe to any live structure, be it the Storage `root`, a child, or a
structure even more deeply nested.
```ts file="liveblocks.config.ts"
import { LiveMap, LiveObject } from "@liveblocks/client";
type Person = LiveObject<{ name: string; age: number }>;
declare global {
interface Liveblocks {
Storage: {
people: LiveMap;
};
}
}
```
```ts
const { root } = await room.getStorage();
const people = root.get("people");
const steven = people.get("steven");
const unsubscribeRoot = room.subscribe(root, (updatedRoot) => {
// ...
});
const unsubscribePeople = room.subscribe(people, (updatedPeople) => {
// ...
});
const unsubscribeSteven = room.subscribe(steven, (updatedSteven) => {
// ...
});
```
#### Listening for nested changes [#listening-for-nested-changes]
It’s also possible to subscribe to a Storage item and all of its children by
passing an optional `isDeep` option in the third argument. In this case, the
callback will be passed a list of updates instead of just the new Storage item.
Each such update is a `{ type, node, updates }` object.
```ts
const { root } = await room.getStorage();
const unsubscribe = room.subscribe(
root,
(storageUpdates) => {
for (const update of storageUpdates) {
const {
type, // "LiveObject", "LiveList", or "LiveMap"
node,
updates,
} = update;
switch (type) {
case "LiveObject": {
// updates["property"]?.type; is "update" or "delete"
// update.node is the LiveObject that has been updated/deleted
break;
}
case "LiveMap": {
// updates["key"]?.type; is "update" or "delete"
// update.node is the LiveMap that has been updated/deleted
break;
}
case "LiveList": {
// updates[0]?.type; is "delete", "insert", "move", or "set"
// update.node is the LiveList that has been updated, deleted, or modified
break;
}
}
}
},
{ isDeep: true }
);
```
#### Using async functions
You use an `async` function inside the subscription callback, though bear in
mind that the callback itself is synchronous, and there’s no guarantee the
`async` function will complete before the callback is run again.
```ts
const { root } = await room.getStorage();
const unsubscribe = room.subscribe(root, (updatedRoot) => {
async function doThing() {
await fetch(/* ... */);
}
doThing();
});
```
If the order of updates is important in your application, and it’s important to
ensure that your `async` function doesn’t start before the previous one
finishes, you can use a package such as
[`async-mutex`](https://www.npmjs.com/package/async-mutex) to help you with
this. Using `runExclusive` will effectively form a queue for all upcoming
updates, guaranteeing serial execution.
```ts
import { Mutex } from "async-mutex";
const { root } = await room.getStorage();
const myMutex = new Mutex();
const unsubscribeUpdates = room.subscribe(root, (root) => {
void myMutex.runExclusive(async () => {
await fetch(/* ... */);
});
});
```
Note that this may cause a performance penalty in your application, as certain
updates will be ignored.
### Room.subscribe("event") [#Room.subscribe.event]
Subscribe to events broadcast by [`Room.broadcastEvent`][]. Takes a callback
that’s run when another user calls [`Room.broadcastEvent`][]. Provides the
`event` along with the `user` and their `connectionId` of the user that sent the
message. Returns an unsubscribe function.
```ts
// User 1
room.broadcastEvent({ type: "REACTION", emoji: "🔥" });
// +++
// User 2
const unsubscribe = room.subscribe("event", ({ event, user, connectionId }) => {
// ^^^^ Will be User 1
if (event.type === "REACTION") {
// Do something
}
});
// +++
```
Unsubscribe function. Call it to cancel the subscription.
Listen to events.
Function that’s called when another user sends an event. Receives the event,
the [`user`](#user-type) that sent the event, and their `connectionId`. If
this event was sent via
[`liveblocks.broadcastEvent`](/docs/api-reference/liveblocks-node#post-broadcast-event)
or the [Broadcast event
API](/docs/api-reference/rest-api-endpoints#post-broadcast-event), `user`
will be `null` and `connectionId` will be `-1`. [Learn
more](#receiving-events-from-the-server)
#### Typing events
When [defining your types](#typing-your-data), you can pass a `RoomEvent` type
to your config file to receive type hints in your app. To define multiple
different custom events, use a union.
```ts
declare global {
interface Liveblocks {
RoomEvent:
| { type: "REACTION"; emoji: string }
| { type: "ACTION"; action: string };
}
}
```
```ts
room.subscribe("event", ({ event, user, connectionId }) => {
if (event.type === "REACTION") {
// Do something
}
if (event.type === "ACTION") {
// Do something else
}
});
```
#### Receiving events from the server [#receiving-events-from-the-server]
Events can be received from the server with either
[`liveblocks.broadcastEvent`](/docs/api-reference/liveblocks-node#post-broadcast-event)
or the
[Broadcast Event API](/docs/api-reference/rest-api-endpoints#post-broadcast-event).
In events sent from the server, `user` will be `null`, and `connectionId` will
be `-1`.
```ts
import { Liveblocks } from "@liveblocks/node";
const liveblocks = new Liveblocks({
secret: "{{SECRET_KEY}}",
});
export async function POST() {
await liveblocks.broadcastEvent({ type: "REACTION", emoji: "🔥" });
}
```
```ts
const unsubscribe = room.subscribe("event", ({ event, user, connectionId }) => {
// `null`, `-1`
console.log(user, connectionId);
});
```
### Room.subscribe("my-presence") [#Room.subscribe.my-presence]
Subscribe to the current user’s Presence. Takes a callback that is called every
time the current user presence is updated with [`Room.updatePresence`][].
Returns an unsubscribe function.
```ts
const unsubscribe = room.subscribe("my-presence", (presence) => {
// Do something
});
```
Unsubscribe function. Call it to cancel the subscription.
Listen to the current user’s presence.
Function that’s called when the current user’s Presence has updated, for
example with [`Room.updatePresence`][]. Receives the updates Presence value.
#### Typing Presence
To type the Presence values you receive, make sure to set your Presence type.
```ts file="liveblocks.config.ts"
declare global {
interface Liveblocks {
Presence: {
status: string;
cursor: { x: number; y: number };
};
}
}
```
The type received in the callback will match the type passed. Learn more under
[typing your data](#typing-your-data).
```ts
const unsubscribe = room.subscribe("my-presence", (presence) => {
// { status: "typing", cursor: { x: 45, y: 67 }
console.log(presence);
});
```
### Room.subscribe("others") [#Room.subscribe.others]
Subscribe to every other users’ updates. Takes a callback that’s called when a
user’s Presence updates, or when they enter or leave the room. Returns an
unsubscribe function.
```ts
const unsubscribe = room.subscribe("others", (others, event) => {
// Do something
});
```
Unsubscribe function. Call it to cancel the subscription.
Listen to others.
Function that’s called when another user’s Presence has updated, for example
with [`Room.updatePresence`][], or an others event has occurred. Receives an
array of [`User`](#user-type) values for each currently connected user. Also
received an object with information about the event that has triggered the
update, [learn more](#listening-for-others-events).
#### Typing Presence
To type the Presence values you receive, make sure to set your Presence type.
```ts file="liveblocks.config.ts"
declare global {
interface Liveblocks {
Presence: {
status: string;
cursor: { x: number; y: number };
};
}
}
```
The type received in the callback will match the type passed. Learn more under
[typing your data](#typing-your-data).
```ts
const unsubscribe = room.subscribe("others", (others, event) => {
// { status: "typing", cursor: { x: 45, y: 67 }
console.log(others[0].presence);
});
```
#### Listening for others events [#listening-for-others-events]
The `event` parameter returns information on why the callback has just run, for
example if their Presence has updated, if they’ve just left or entered the room,
or if the current user has disconnected.
```ts
const unsubscribe = room.subscribe("others", (others, event) => {
if (event.type === "leave") {
// A user has left the room
// event.user;
}
if (event.type === "enter") {
// A user has entered the room
// event.user;
}
if (event.type === "update") {
// A user has updated
// event.user;
// event.updates;
}
if (event.type === "reset") {
// A disconnection has occurred and others has reset
}
});
```
#### Live cursors
Here’s a basic example showing you how to render live cursors.
[`Room.updatePresence`](/docs/api-reference/liveblocks-client#Room.updatePresence)
is being used to update each user’s cursor position.
```ts file="liveblocks.config.ts"
declare global {
interface Liveblocks {
Presence: {
cursor: { x: number; y: number };
};
}
}
```
```ts
const { room, leave } = client.enterRoom("my-room-id");
// Call this to update the current user’s Presence
function updateCursorPosition({ x, y }) {
room.updatePresence({ cursor: { x, y } });
}
const others = room.getOthers();
// Run __renderCursor__ when any other connected user updates their presence
const unsubscribe = room.subscribe("others", (others, event) => {
for (const { id, presence } of others) {
const { x, y } = presence.cursor;
__renderCursor__(id, { x, y });
}
}
// Handle events and rendering
// ...
```
Check our [examples page](/examples/browse/cursors) for live demos.
### Room.subscribe("status") [#Room.subscribe.status]
Subscribe to WebSocket connection status updates. Takes a callback that is
called whenever the connection status changes. Possible value are: `initial`,
`connecting`, `connected`, `reconnecting`, or `disconnected`. Returns an
unsubscribe function.
```ts
const unsubscribe = room.subscribe("status", (status) => {
// "connected"
console.log(status);
});
```
Unsubscribe function. Call it to cancel the subscription.
Listen to status updates.
void`}
required
>
Function that’s called when the room’s connection status has changed. It can return one of five values:
- `"initial"` The room has not attempted to connect yet.
- `"connecting"` The room is currently authenticating or connecting.
- `"connected"` The room is connected.
- `"reconnecting"` The room has disconnected, and is trying to connect again.
- `"disconnected"` The room is disconnected, and is no longer attempting to connect.
#### When to use status
Status is a low-level API that exposes the WebSocket’s connectivity status. You
can use this, for example, to update a connection status indicator in your UI.
It would be normal for a client to briefly lose the connection and restore it
with quick `connected` → `reconnecting` → `connected` status jumps.
```ts
let indicator = "⚪";
const unsubscribe = room.subscribe("status", (status) => {
switch (status) {
case "connecting":
indicator = "🟡";
break;
case "connected":
indicator = "🟢";
break;
// ...
}
});
```
If you’d like to let users know that there may be connectivity issues, don’t use
this API, but instead refer to [`Room.subscribe("lost-connection")`][] which was
specially built for this purpose.
Do not use this API to detect when Storage or Presence are initialized or
loaded. "Connected" does not guarantee that Storage or Presence are ready. To
detect when Storage is loaded, rely on awaiting the [`Room.getStorage`][]
promise or using the [`Room.subscribe("storage-status")`][] event.
### Room.subscribe("lost-connection") [#Room.subscribe.lost-connection]
A special-purpose event that will fire when a previously connected Liveblocks
client has lost connection, for example due to a network outage, and was unable
to recover quickly. This event is
[designed to help improve UX for your users](#when-to-use-lost-connection-events),
and will not trigger on short interruptions, those that are less than
[5 seconds by default](#setting-lost-connection-timeout). The event only
triggers if a previously connected client disconnects.
```ts
const unsubscribe = room.subscribe("lost-connection", (event) => {
// "lost"
console.log(event);
});
```
Unsubscribe function. Call it to cancel the subscription.
Listen to lost connection events.
void`}
required
>
Function that’s called when a room’s lost connection event has been triggered. It can return one of three values:
- `"lost"` A connection has been lost for longer than [`lostConnectionTimeout`][].
- `"restored"` The connection has been restored again.
- `"failed"` The room has been unable to reconnect again, and is no longer trying. This may happen if a user’s
network has recovered, but the room’s authentication values no longer allow them to enter.
#### When to use lost connection events [#when-to-use-lost-connection-events]
Lost connections events allows you to build high-quality UIs by warning your
users that the application is still trying to re-establish the connection, for
example through a toast notification. You may want to take extra care in the
mean time to ensure their changes won’t go unsaved, or to help them understand
why they’re not seeing updates made by others yet.
When this happens, this callback is called with the event `lost`. Then, once the
connection restores, the callback will be called with the value `restored`. If
the connection could definitively not be restored, it will be called with
`failed` (uncommon).
```ts
import { toast } from "my-preferred-toast-library";
const unsubscribe = room.subscribe("lost-connection", (event) => {
switch (event) {
case "lost":
toast.warn("Still trying to reconnect...");
break;
case "restored":
toast.success("Successfully reconnected again!");
break;
case "failed":
toast.error("Could not restore the connection");
break;
}
});
```
#### Setting lost connection timeout [#setting-lost-connection-timeout]
The [`lostConnectionTimeout`][] configuration option will determine how quickly
the event triggers after a connection loss occurs. By default, it’s set to
`5000`ms, which is 5 seconds.
```ts
import { createClient } from "@liveblocks/client";
const client = createClient({
// Throw lost-connection event after 5 seconds offline
lostConnectionTimeout: 5000,
// ...
});
```
### Room.subscribe("error") [#Room.subscribe.error]
Subscribe to unrecoverable room connection errors. This event will be emitted
immediately before the client disconnects and won’t try reconnecting again.
Returns an unsubscribe function. If you’d like to retry connecting, call
[`room.reconnect`][].
```ts
const unsubscribe = room.subscribe("error", (error) => {
switch (error.context.code) {
case -1:
// Authentication error
break;
case 4001:
// Could not connect because you don’t have access to this room
break;
case 4005:
// Could not connect because room was full
break;
case 4006:
// The room ID has changed, get the new room ID (use this for redirecting)
const newRoomId = error.message;
break;
default:
// Unexpected error
break;
}
});
```
Unsubscribe function. Call it to cancel the subscription.
Listen to error events.
void`}
required
>
Function that’s called when an unrecoverable error event has been triggered. `error.code` can return one of these
values:
- `-1` Authentication error.
- `4001` Could not connect because you don’t have access to this room.
- `4005` Could not connect because room was full.
- `4006` The room ID has changed.
#### When to use error events
You can use this event to trigger a “Not allowed” screen/dialog. It can also be
helpful for implementing a redirect to another page.
```ts
const unsubscribe = room.subscribe("error", (error) => {
// Could not connect because you don’t have access to this room
if (error.context.code === 4001)
return __displayForbiddenEntryDialog__();
}
});
```
#### When a room ID has changed
When a room ID has been changed with
[`liveblocks.updateRoomId`](/docs/api-reference/liveblocks-node#post-rooms-update-roomId)
or the
[Update Room ID API](/docs/api-reference/rest-api-endpoints#post-rooms-update-roomId),
`error.message` will contain the new room ID.
```ts
const unsubscribe = room.subscribe("error", (error) => {
// The room ID has changed, get the new room ID
if (error.context.code === 4006)
const newRoomId = error.message;
return __redirect__(`/app/${newRoomId}`)
}
});
```
### Room.subscribe("history") [#Room.subscribe.history]
Subscribe to the current user’s history changes. Returns an unsubscribe
function.
```ts
const unsubscribe = room.subscribe("history", ({ canUndo, canRedo }) => {
// Do something
});
```
Unsubscribe function. Call it to cancel the subscription.
Listen to history events.
void`}
required
>
Function that’s called when the current user’s history changes. Returns
booleans that describe whether the user can use
[undo](/docs/api-reference/liveblocks-client#Room.history.undo) or
[redo](/docs/api-reference/liveblocks-client#Room.history.redo).
### Room.subscribe("storage-status") [#Room.subscribe.storage-status]
Subscribe to Storage status changes. Use this to tell whether Storage has been
synchronized with the Liveblocks servers. Returns an unsubscribe function.
```ts
const unsubscribe = room.subscribe("storage-status", (status) => {
switch (status) {
case "not-loaded":
// Storage has not been loaded yet
break;
case "loading":
// Storage is currently loading
break;
case "synchronizing":
// Local Storage changes are being synchronized
break;
case "synchronized":
// Local Storage changes have been synchronized
break;
}
});
```
Unsubscribe function. Call it to cancel the subscription.
Listen to Storage status events.
void`}
required
>
Function that’s called when the current user’s Storage updated status have
changed. `status` can be one of four types.
- `"not-loaded` - Storage has not been loaded yet as [`getStorage`][] has not been called.
- `"loading"` - Storage is currently loading for the first time.
- `"synchronizing"` - Local Storage changes are currently being synchronized.
- `"synchronized"` - Local Storage changes have been synchronized
### Room.batch
Batches Storage and Presence modifications made during the given function. Each
modification is grouped together, which means that other clients receive the
changes as a single message after the batch function has run. When undoing or
redoing these changes, the entire batch will be undone/redone together instead
of atomically.
```ts
const { root } = await room.getStorage();
room.batch(() => {
root.set("x", 0);
room.updatePresence({ cursor: { x: 100, y: 100 } });
});
```
Returns the return value from the callback.
A callback containing every Storage and Presence notification that will be
part of the batch. Cannot be an `async` function.
#### When to batch updates
For the most part, _you don’t need to batch updates_. For example, given a
[whiteboard application](/examples/browse/whiteboard), it’s perfectly fine to
update a note’s position on the board multiple times per second, in separate
updates. However, should you implement a “Delete all” button, that may delete 50
notes at once, this is where you should use a batch.
```ts
const { root } = await room.getStorage();
const notes = root.get("notes");
// ✅ Batch simultaneous changes together
room.batch(() => {
for (const noteId of notes.keys()) {
notes.delete(noteId);
}
});
```
This batch places each
[`LiveMap.delete`](/docs/api-reference/liveblocks-client#LiveMap.delete) call
into a single WebSocket update, instead of sending multiple updates. This will
be much quicker.
#### Batching groups history changes
Batching changes will also group changes into a single history state.
```ts
const { root } = await room.getStorage();
const pet = root.set("pet", new LiveObject({ name: "Fido", age: 5 }));
// ✅ Batch groups changes into one
room.batch(() => {
pet.set("name", "Felix");
pet.set("age", 10);
});
// { name: "Felix", age: 10 }
pet.toImmutable();
room.history.undo();
// { name: "Fido", age: 5 }
pet.toImmutable();
```
#### Doesn’t work with async functions
Note that `room.batch` cannot take an `async` function.
```tsx
// ❌ Won’t work
room.batch(async () => {
// ...
});
// ✅ Will work
room.batch(() => {
// ...
});
```
### Room.history
Room’s history contains functions that let you undo and redo operations made to
Storage and Presence on the current client. Each user has a separate history
stored in memory, and history is reset when the page is reloaded.
```ts
const { undo, redo, pause, resume /*, ... */ } = room.history;
```
Note that to undo or redo in Yjs, you must use a separate history manager,
[`Y.UndoManager`](https://docs.yjs.dev/api/undo-manager).
#### Add Presence to history
By default, history is only enabled for Storage. However, you can use the
`addToHistory` option to additionally
[add Presence state to history](/docs/api-reference/liveblocks-client#add-presence-to-history).
```tsx
room.updatePresence({ color: "blue" }, { addToHistory: true });
```
### Room.history.undo
Reverts the last operation. It does not impact operations made by other clients,
and will only undo changes made by the current client.
```ts
const person = new LiveObject();
person.set("name", "Pierre");
person.set("name", "Jonathan");
room.history.undo();
// "Pierre"
root.get("name");
```
_Nothing__None_
### Room.history.redo
Restores the last undone operation. It does not impact operations made by other
clients, and will only restore changes made by the current client.
```ts
const person = new LiveObject();
person.set("name", "Pierre");
person.set("name", "Jonathan");
room.history.undo();
room.history.redo();
// "Jonathan"
root.get("name");
```
_Nothing__None_
### Room.history.canUndo
Returns true or false, depending on whether there are any operations to undo.
Helpful for disabling undo buttons.
```ts
const person = new LiveObject();
person.set("name", "Pierre");
// true
room.history.canUndo();
room.history.undo();
// false
room.history.canUndo();
```
Whether there is an undo operation in the current history stack.
_None_
### Room.history.canRedo
Returns true or false, depending on whether there are any operations to redo.
Helpful for disabling redo buttons.
```ts
const person = new LiveObject();
person.set("name", "Pierre");
// false
room.history.canRedo();
room.history.undo();
// true
room.history.canRedo();
```
Whether there is a redo operation in the current history stack.
_None_
### Room.history.clear
Clears the undo and redo stacks for the current client. Explicitly clearing
history resets the ability to undo beyond the current document state. Other
clients’ histories are unaffected.
```ts
const person = new LiveObject();
person.set("name", "Pierre");
// true
room.history.canUndo();
room.history.clear();
// false
room.history.canUndo();
```
_Nothing__None_
### Room.history.pause
All future modifications made on the Room will be merged together to create a
single history item until resume is called.
```ts
const info = new LiveObject({ time: "one" });
room.history.pause();
info.set("time", "two");
info.set("time", "three");
room.history.resume();
room.history.undo();
// "one"
room.get("time");
```
_Nothing__None_
### Room.history.resume
Resumes history after a [pause](#Room.history.pause). Modifications made on the
Room are not merged into a single history item any more.
```ts
const info = new LiveObject({ time: "one" });
room.history.pause();
info.set("time", "two");
info.set("time", "three");
room.history.resume();
room.history.undo();
// "one"
room.get("time");
```
_Nothing__None_
### Room.connect
Connect the local room instance to the Liveblocks server. Does nothing if the
room is already connecting, reconnecting or connected. We don’t recommend using
this API directly.
```ts
room.connect();
```
_Nothing__None_
### Room.reconnect
Reconnect the local room instance to the Liveblocks server, using a new
WebSocket connection.
```ts
room.reconnect();
```
_Nothing__None_
### Room.disconnect
Disconnect the local room instance from the Liveblocks server. The room instance
will remain functional (for example, it will still allow local presence or
storage mutations), but since it’s no longer connected, changes will not be
persisted or synchronized until the room instance is reconnected again. We don’t
recommend using this API directly.
```ts
room.disconnect();
```
_Nothing__None_
## Comments
### Room.getThreads
Returns threads, and their associated inbox notifications and subscriptions,
that are in the current room. It also returns the request date that can be used
for subsequent polling. It’s possible to filter for
[a thread’s resolved status](#filtering-resolved-status) and using
[custom metadata](#filtering-metadata).
```ts
const { threads, inboxNotifications, requestedAt } = await room.getThreads();
// [{ id: "th_s436g8...", type: "thread" }, ...]
console.log(threads);
// [{ id: "in_fwh3d4...", kind: "thread", }, ...]
console.log(inboxNotifications);
```
Threads within the current room.
Inbox notifications associated with the threads.
Subscriptions associated with the threads.
The request date to use for subsequent polling.
Only return `resolved` or `unresolved` threads. [Learn more](#filtering-resolved-status).
Only return `subscribed` or `unsubscribed` threads. [Learn more](#filtering-subscribed-status).
Only return threads containing the custom metadata. Metadata is set yourself when creating a thread, for example `{ priority: "HIGH" }`. [Learn more](#filtering-metadata).
#### Filtering resolved status [#filtering-resolved-status]
You can filter threads by those that are resolved, or unresolved, by passing a
`boolean` to `query.resolved`.
```ts
// Filtering for threads that are unresolved
const threads = await room.getThreads({
query: {
// +++
resolved: false,
// +++
},
});
```
#### Filtering subscribed status [#filtering-subscribed-status]
You can filter threads by those that the user is subscribed to, or not, by
passing a `boolean` to `query.subscribed`.
```ts
// Filtering for threads that the user is subscribed to
const threads = await room.getThreads({
query: {
// +++
subscribed: true,
// +++
},
});
```
#### Filtering metadata [#filtering-metadata]
You can define custom metadata when
[creating a thread](/docs/api-reference/liveblocks-client#Room.createThread),
and the `query.metadata` option allows you to return only threads that match.
```ts
// Creating a thread with `priority` metadata
await room.createThread({
body: {
// ...
},
// +++
metadata: { priority: "HIGH" },
// +++
});
// Filtering for threads with the same metadata
const threads = await room.getThreads({
query: {
// +++
metadata: { priority: "HIGH" },
// +++
},
});
```
You can also filter for metadata that begins with a specific string.
```ts
// Creating a thread with `{ assigned: "sales:stacy" } metadata
await room.createThread({
body: {
// ...
},
// +++
metadata: { assigned: "sales:stacy" },
// +++
});
// Filtering for threads with `assigned` metadata that starts with `sales:`
const threads = await room.getThreads({
query: {
// +++
metadata: {
assigned: {
startsWith: "sales:",
},
},
// +++
},
});
```
You can also filter for metadata using numeric operators.
```ts
// Creating a thread with `{ posX: 87, level: 5 } metadata
await room.createThread({
body: {
// ...
},
// +++
metadata: { posX: 87, level: 5 },
// +++
});
// Filtering for threads with `posX` greater than 50 and lower than 100, and level greater than or equal to 5
const threads = await room.getThreads({
query: {
// +++
metadata: {
posX: {
gt: 50,
lt: 100,
},
level: {
gte: 5,
},
},
// +++
},
});
```
### Room.getThreadsSince
Returns threads, and their associated inbox notifications and subscriptions,
that have been updated or deleted since the requested date. Helpful when used in
combination with [`Room.getThreads`](#Room.getThreads) to initially fetch all
threads, then receive updates later.
```ts
const initial = await room.getThreads();
const { threads, inboxNotifications, subscriptions, requestedAt } =
await room.getThreadsSince({ since: initial.requestedAt });
// { updated: [{ id: "th_s4368s...", type: "thread" }, ...], deleted: [...] }
console.log(threads);
// { updated: [{ id: "in_ds83hs...", kind: "thread", }, ...], deleted: [...] }
console.log(inboxNotifications);
// { updated: [{ subjectId: "th_s4368s...", kind: "thread", }, ...], deleted: [...] }
console.log(subscriptions);
```
Threads that have been updated or deleted since the requested date.
Inbox notifications that have been updated or deleted since the requested
date.
Subscriptions that have been updated or deleted since the requested date.
The request date to use for subsequent polling.
Only return threads that have been updated or deleted after this date.
### Room.getThread
Returns a thread and its associated inbox notification and subscription, from
its ID, if it exists.
```ts
const { thread, inboxNotification, subscription } =
await room.getThread("th_xxx");
```
The thread ID can be retrieved from existing threads.
```ts
const newThread = await room.createThread(/* ... */);
const { thread, inboxNotification } = await room.getThread(newThread.id);
```
The requested thread, or `undefined` if it doesn’t exist.
The inbox notification associated with the thread, or `undefined` if it
doesn’t exist.
The subscription associated with the thread, or `undefined` if it doesn’t
exist.
The ID of the thread you want to retrieve.
### Room.createThread
Creates a thread, and its initial comment, in the current room. A comment’s body
is an array of paragraphs, each containing child nodes, learn more under
[creating thread content](#creating-thread-content).
```ts
const thread = await room.createThread({
body: {
version: 1,
content: [{ type: "paragraph", children: [{ text: "Hello" }] }],
},
});
```
The thread that has been created.
The content of the comment, see [creating thread
content](#creating-thread-content).
The IDs of the comment’s attachments.
Custom metadata to be attached to the 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.
### new LiveObject [#LiveObject.constructor]
Create an empty `LiveObject`
```ts
import { LiveObject } from "@liveblocks/client";
const object = new LiveObject();
```
Create a `LiveObject` with initial data.
```ts
import { LiveObject } from "@liveblocks/client";
const object = new LiveObject({ firstName: "Margaret", lastName: "Hamilton" });
```
The newly created `LiveObject`.
The initial value for the `LiveObject`. Can contain JSON-serializable data
and other Liveblocks conflict-free data structures.
#### Add a LiveObject to Storage
The Storage root is `LiveObject` itself, so you can use [`LiveObject.set`]() to
add a new property to your root. If you’ve [typed Storage](#typing-storage)
you’ll have type hints as you build.
```ts
import { LiveObject } from "@liveblocks/client";
const { root } = await room.getStorage();
const person = new LiveObject({ name: "Alicia" });
root.set("person", person);
```
### delete [#LiveObject.delete]
Delete a property from the `LiveObject`
```ts
const object = new LiveObject({ firstName: "Ada", lastName: "Lovelace" });
object.delete("lastName");
// { firstName: "Ada" }
object.toImmutable();
```
_Nothing_
The key of the property you’re deleting. If the property doesn’t exist,
nothing will occur.
### get [#LiveObject.get]
Get a property from the `LiveObject`.
```ts
const object = new LiveObject({ firstName: "Ada", lastName: "Lovelace" });
// "Ada"
object.get("firstName");
```
The value of the property. Returns `undefined` if it doesn’t exist.
The key of the property you’re getting.
### set [#LiveObject.set]
Adds or updates a property with the specified key and a value.
```ts
const object = new LiveObject({ firstName: "Marie" });
object.set("lastName", "Curie");
// { firstName: "Ada", lastName: "Curie" }
object.toImmutable();
```
_Nothing_
The key of the property you’re setting.
The value of the property you’re setting. Can contain JSON-serializable data
and other Liveblocks conflict-free data structures.
### update [#LiveObject.update]
Adds or updates multiple properties at once. Nested changes to other Storage
types will not be applied.
```ts
const object = new LiveObject({ firstName: "Grace" });
object.update({ lastName: "Hopper", job: "Computer Scientist" });
// { firstName: "Grace", lastName: "Hopper", job: "Computer Scientist" }
object.toImmutable();
```
_Nothing_
The keys and values you’re updating. Can contain JSON-serializable data and
other Liveblocks conflict-free data structures. Nested changes to other
Storage types will not be applied.
### clone [#LiveObject.clone]
Returns a deep copy of the `LiveObject` that can be inserted elsewhere in the
Storage tree.
```ts
const obj = new LiveObject(/* ... */);
root.set("a", obj);
root.set("b", obj.clone());
```
The cloned `LiveObject`.
_None_
### toImmutable [#LiveObject.toImmutable]
Returns an immutable JavaScript object that is equivalent to the `LiveObject`.
Nested values will also be immutable. Calling this method multiple times has no
performance penalty. It will return the same cached immutable value as long as
its (nested) contents have not changed.
```ts
const liveObject = new LiveObject({
firstName: "Grace",
lastName: "Hopper",
hobbies: new LiveList(["reading", "piano"]),
});
// {
// firstName: "Grace",
// lastName: "Hopper",
// hobbies: ["reading", "piano"]
// }
liveObject.toImmutable();
```
Returns a JavaScript object in the shape of your data structure.
`LiveObject` is converted to an object, `LiveMap` to a map, and `LiveList`
to an array.
_None_
### toObject [#LiveObject.toObject]
Starting with 0.18, we recommend [`toImmutable`][] instead. It’s faster, cached,
and leads to fewer surprises.
Transform the `LiveObject` into a normal JavaScript object.
```ts
const liveObject = new LiveObject({ firstName: "Grace", lastName: "Hopper" });
liveObject.toObject();
// { firstName: "Grace", lastName: "Hopper" }
```
Please note that this method won’t recursively convert Live structures, which
may be surprising:
```ts
const liveObject = new LiveObject({
animals: new LiveList(["🦁", "🦊", "🐵"]),
});
liveObject.toObject();
// { animals: } // ❗️
```
## LiveMap
The `LiveMap` class is similar to a
[JavaScript Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
that is synchronized on all clients. Use this for indexing values that all have
the same structure. For example, to store an index of `Person` values by their
name. To add typing, read more under [typing Storage](#typing-storage).
```ts
type Shapes = LiveMap>;
```
Keys are strings, and values can contain other Storage structures, or
JSON-serializable data. If multiple clients update the same property
simultaneously, the last modification received by the Liveblocks servers is the
winner.
### new LiveMap [#LiveMap.constructor]
Create an empty `LiveMap`.
```ts
const map = new LiveMap();
```
Create a `LiveMap` with initial data.
```ts
const map = new LiveMap([
["nimesh", "developer"],
["pierre", "designer"],
]);
```
The newly created `LiveMap`.
The initial value for the `LiveMap`. An array of tuples, each containing a
key and a value. The values can contain JSON-serializable data and other
Liveblocks conflict-free data structures.
#### Add a LiveMap to Storage
The Storage root is a `LiveObject`, so you can create a new `LiveMap` then use
[`LiveObject.set`]() to add it to your root. If you’ve
[typed Storage](#typing-storage) you’ll have type hints as you build.
```ts
import { LiveMap } from "@liveblocks/client";
const { root } = await room.getStorage();
const people = new LiveMap([
["vincent", "engineer"],
["marc", "designer"],
]);
root.set("people", people);
```
### delete [#LiveMap.delete]
Removes the specified element by key. Returns true if an element existed and has
been removed, or false if the element does not exist.
```ts
const map = new LiveMap([
["nimesh", "developer"],
["pierre", "designer"],
]);
// true
map.delete("nimesh");
// Map { "pierre" => "designer" }
map.toImmutable();
```
If the element existed and was removed.
The key of the element you’re deleting. If the element doesn’t exist,
nothing will occur.
### entries [#LiveMap.entries]
Returns a new
[Iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
object that contains the `[key, value]` pairs for each element.
```ts
for (const [key, value] of map.entries()) {
// Iterate over all the keys and values of the map
}
```
If your TypeScript project targets es5 or lower, you’ll need to
enable the --downlevelIteration option to use this API.
A new Iterator object for the `LiveMap`, containing the `[key, value]` pairs
for each element.
_None_
### forEach [#LiveMap.forEach]
Executes a provided function once per each key/value pair in the Map object, in
insertion order.
```ts
const map = new LiveMap([
["nimesh", "developer"],
["pierre", "designer"],
]);
// "developer", "designer"
map.forEach((value, key, liveMap) => console.log(value));
```
_Nothing_
A callback for each entry. The callback is passed the current `value`,
`key`, and the `LiveMap`. Return values are ignored.
### get [#LiveMap.get]
Returns a specified element from the `LiveMap`. Returns `undefined` if the key
can’t be found.
```ts
const map = new LiveMap([
["nimesh", "developer"],
["pierre", "designer"],
]);
// "developer"
map.get("nimesh");
// undefined
map.get("alicia");
```
The value of the entry. Returns `undefined` if it doesn’t exist.
The key of the entry you’re getting.
### has [#LiveMap.has]
Returns a boolean indicating whether an element with the specified key exists or
not.
```ts
const map = new LiveMap([
["nimesh", "developer"],
["pierre", "designer"],
]);
// true
map.has("nimesh");
// false
map.has("alicia");
```
Whether the entry exists.
The key of the entry you’re getting.
### keys [#LiveMap.keys]
Returns a new
[Iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
object that contains the keys for each element.
```ts
for (const key of map.keys()) {
// Iterate over all the keys and values of the map
}
```
If your TypeScript project targets es5 or lower, you’ll need to
enable the --downlevelIteration option to use this API.
A new Iterator object for the `LiveMap`, containing the keys of each entry.
_None_
### set [#LiveMap.set]
Adds or updates an element with a specified key and a value.
```ts
const map = new LiveMap();
map.set("vincent", "engineer");
// Map { "vincent" => "engineer" }
map.toImmutable();
```
_Nothing_
The key of the entry you’re setting.
The value of the entry you’re setting. Can contain JSON-serializable data
and other Liveblocks conflict-free data structures.
### size [#LiveMap.size]
Returns the number of elements in the `LiveMap`.
```ts
const map = new LiveMap([
["nimesh", "developer"],
["pierre", "designer"],
]);
// 2
map.size;
```
The number of entries in the `LiveMap`
_N/A_
### values [#LiveMap.values]
Returns a new
[Iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
object that contains the values for each element.
```ts
for (const value of map.values()) {
// Iterate over all the values of the map
}
```
If your TypeScript project targets es5 or lower, you’ll need to
enable the --downlevelIteration option to use this API.
A new Iterator object for the `LiveMap`, containing the values of each
entry.
_None_
### clone [#LiveMap.clone]
Returns a deep copy of the `LiveMap` that can be inserted elsewhere in the
Storage tree.
```ts
const map = new LiveMap(/* ... */);
root.set("a", map);
root.set("b", map.clone());
```
The cloned `LiveMap`.
_None_
### toImmutable [#LiveMap.toImmutable]
Returns an immutable ES6 Map that is equivalent to the `LiveMap`. Nested values
will also be immutable. Calling this method multiple times has no performance
penalty. It will return the same cached immutable value as long as its (nested)
contents have not changed.
```ts
const map = new LiveMap([
["florent", new LiveObject({ role: "engineer" })],
["marc", new LiveObject({ role: "designer" })],
]);
// Map {
// "florent" => { role: "engineer" },
// "marc" => { role: "designer" },
// }
map.toImmutable();
```
Returns a JavaScript object in the shape of your data structure. `LiveMap`
is converted to a map, `LiveObject` to an object, and `LiveList` to an
array.
_None_
## LiveList
The `LiveList` class represents an ordered collection of items that is
synchronized across clients. To add typing, read more under
[typing Storage](#typing-storage).
```ts
type Names = LiveList;
```
Items can contain other Storage structures, or JSON-serializable data.
### new LiveList [#LiveList.constructor]
Create an empty `LiveList`.
```ts
const list = new LiveList();
```
Create a `LiveList` with initial data.
```ts
const list = new LiveList(["adrien", "jonathan"]);
```
The newly created `LiveList`.
The initial array of values for the `LiveList`. Can contain
JSON-serializable data and other Liveblocks conflict-free data structures.
### clear [#LiveList.clear]
Removes all the elements.
```ts
const list = new LiveList(["adrien", "jonathan"]);
list.clear();
// []
list.toImmutable();
```
_Nothing__None_
### delete [#LiveList.delete]
Deletes the element living at the specified index locally. If the index doesn't
exist, an `Error` is thrown.
```ts
const list = new LiveList(["adrien", "jonathan"]);
list.delete(0);
// ["jonathan"]
list.toImmutable();
```
This operation uses ID-based semantics, not position-based. When called, it
reads the item at the specified index from the local state, then sends a "delete
item with ID X" instruction to the server.
If clients A and B both see a LiveList containing `["foo", "bar"]`, and client A
calls `.insert("qux", 0)`, while client B simultaneously calls `.delete(0)`, the
end result will always be `["qux", "bar"]` on both clients, and never
`["foo", "bar"]`.
_Nothing_
The index of the property you’re deleting. If the property doesn’t exist, an
`Error` is thrown.
### every [#LiveList.every]
Tests whether all elements pass the test implemented by the provided function.
Returns true if the predicate function returns a truthy value for every element.
Otherwise, false.
```ts
const list = new LiveList([0, 2, 4]);
// true
list.every((i) => i % 2 === 0);
list.push(5);
// false
list.every((i) => i % 2 === 0);
```
Whether all elements pass the test implemented by the provided function.
A function to execute for each item in the array. It should return a truthy
value to indicate the element passes the test, and a falsy value otherwise.
The function is passed the `value` of the item and its current `index`.
### filter [#LiveList.filter]
Creates an array with all elements that pass the test implemented by the
provided function.
```ts
const list = new LiveList([0, 1, 2, 3, 4]);
// [0, 2, 4]
list.filter((i) => i % 2 === 0);
```
An array containing each item of the `LiveList` that passed the test
implemented by the provided function.
A function to execute for each item in the array. It should return a truthy
value to indicate the element passes the test, and a falsy value otherwise.
The function is passed the `value` of the item and its current `index`.
### find [#LiveList.find]
Returns the first element that satisfies the provided testing function. If no
item passes the test, `undefined` is returned.
```ts
const list = new LiveList(["apple", "lemon", "tomato"]);
// "lemon"
list.find((value, index) => value.startsWith("l"));
```
The item that has been found. If no item passes the test, `undefined` is
returned.
A function to execute for each item in the array. It should return a truthy
value to indicate the element passes the test, and a falsy value otherwise.
The function is passed the `value` of the item and its current `index`.
### findIndex [#LiveList.findIndex]
Returns the index of the first element in the `LiveList` that satisfies the
provided testing function. If no item passes the test, `-1` is returned.
```ts
const list = new LiveList(["apple", "lemon", "tomato"]);
// 1
list.findIndex((value, index) => value.startsWith("l"));
```
The index of the item that has been found. If no item passes the test, `-1`
is returned.
A function to execute for each item in the array. It should return a truthy
value to indicate the element passes the test, and a falsy value otherwise.
The function is passed the `value` of the item and its current `index`.
### forEach [#LiveList.forEach]
Executes a provided function once for each element.
```ts
const list = new LiveList(["adrien", "jonathan"]);
// "adrien", "jonathan"
list.forEach((item) => console.log(item));
```
_Nothing_
A callback for each item. The callback is passed the current `value` and
`index`. Return values are ignored.
### get [#LiveList.get]
Get the element at the specified index. Returns `undefined` if the index doesn’t
exist.
```ts
const list = new LiveList(["adrien", "jonathan"]);
// "jonathan"
list.get(1);
```
The value of the item at the index. Returns `undefined` if it doesn’t exist.
The index of the item you’re getting.
### indexOf [#LiveList.indexOf]
Returns the first index at which a given element can be found in the `LiveList`.
Returns `-1` if it is not present.
```ts
const list = new LiveList(["adrien", "jonathan"]);
// 1
list.indexOf("jonathan");
// undefined
list.indexOf("chris");
```
The index of the item. Returns `-1` if it doesn’t exist.
The item you’re locating.
The index to start the search at.
### insert [#LiveList.insert]
Inserts one element at a specified index. Throws an `Error` if the index is out
of bounds.
```ts
const list = new LiveList(["adrien", "jonathan"]);
list.insert("chris", 1);
// ["adrien", "chris", "jonathan"]
list.toImmutable();
```
_Nothing_
The value of the item you’re inserting.
The index to insert the item into.
### lastIndexOf [#LiveList.lastIndexOf]
Returns the last index at which a given element can be found in the `LiveList`,
or -1 if it is not present. The `LiveList` is searched backwards, starting at
fromIndex. Returns `-1` if it is not present.
```ts
const list = new LiveList(["adrien", "jonathan", "adrien"]);
// 2
list.indexOf("adrien");
// undefined
list.indexOf("chris");
```
The index of the item. Returns `-1` if it doesn’t exist.
The item you’re locating.
The index at which to start searching backwards.
### length [#LiveList.length]
Returns the number of elements.
```ts
const list = new LiveList(["adrien", "jonathan"]);
// 2
list.length;
```
The number of items in the `LiveList`.
_N/A_
### map [#LiveList.map]
Creates an array populated with the results of calling a provided function on
every element.
```ts
const list = new LiveList(["apple", "lemon", "tomato"]);
// ["APPLE", "LEMON", "TOMATO"]
list.map((value, index) => value.toUpperCase());
```
The array of each item has been transformed by the callback function.
A callback for each item. The callback is passed the current `value` and
`index`. Return values are used in the returned array.
### move [#LiveList.move]
Moves one element at a specified index.
```ts
const list = new LiveList(["adrien", "chris", "jonathan"]);
list.move(2, 0);
// ["jonathan", "adrien", "chris"]
list.toImmutable();
```
_Nothing_
The index of the item to move.
The index where the element should be after moving.
### push [#LiveList.push]
Adds one element to the end of the `LiveList`.
```ts
const list = new LiveList(["adrien", "jonathan"]);
list.push("chris");
// ["adrien", "jonathan", "chris"]
list.toImmutable();
```
_Nothing_
The item to add to the end of the `LiveList`.
### set [#LiveList.set]
Replace one element at the specified index.
```ts
const list = new LiveList(["adrien", "jonathan"]);
list.set(1, "chris");
// equals ["adrien", "chris"]
list.toImmutable();
```
### some [#LiveList.some]
Tests whether at least one element in the `LiveList` passes the test implemented
by the provided function.
```ts
const list = new LiveList(["apple", "lemon", "tomato"]);
// true
list.some((value, index) => value.startsWith("l"));
// false
list.some((value, index) => value.startsWith("x"));
```
Whether any elements pass the test implemented by the provided function.
A function to execute for each item in the array. It should return a truthy
value to indicate the element passes the test, and a falsy value otherwise.
The function is passed the `value` of the item and its current `index`.
### clone [#LiveList.clone]
Returns a deep copy of the `LiveList` that can be inserted elsewhere in the
Storage tree.
```ts
const list = new LiveList(/* ... */);
root.set("a", list);
root.set("b", list.clone());
```
The cloned `LiveList`.
_None_
### toImmutable [#LiveList.toImmutable]
Returns an immutable JavaScript array that is equivalent to the `LiveList`.
Nested values will also be immutable. Calling this method multiple times has no
performance penalty. It will return the same cached immutable value as long as
its (nested) contents have not changed.
```ts
const list = new LiveList([
new LiveObject({ name: "Olivier" }),
new LiveObject({ name: "Vincent" }),
]);
// [
// { name: "Olivier" },
// { name: "Vincent" },
// ]
list.toImmutable();
```
Returns a JavaScript object in the shape of your data structure. `ListList`
is converted to an array, `LiveObject` to an object, and `LiveMap` to a map.
_None_
### toArray [#LiveList.toArray]
Starting with 0.18, we recommend [`toImmutable`][] instead. It’s faster, cached,
and leads to fewer surprises.
Transforms the `LiveList` into a normal JavaScript array.
```ts
const list = new LiveList(["🦁", "🦊", "🐵"]);
list.toArray();
// ["🦁", "🦊", "🐵"]
```
Please note that this method won’t recursively convert Live structures, which
may be surprising:
```ts
const list = new LiveList([
new LiveObject({ firstName: "Grace", lastName: "Hopper" }),
]);
list.toArray();
// [ ] // ❗️
```
## Resolvers
### invalidateUsers
`client.resolvers.invalidateUsers` can be used to invalidate some or all users
that were previously cached by [`resolveUsers`](#createClientResolveUsers).
It can be used when updating the current user’s avatar for example, to instantly
refresh the user data everywhere without having to perform a page reload.
```tsx
// Invalidate all users
client.resolvers.invalidateUsers();
// Only invalidate "user-0" and "user-1"
client.resolvers.invalidateUsers(["user-0", "user-1"]);
```
### invalidateRoomsInfo
`client.resolvers.invalidateRoomsInfo` can be used to invalidate some or all
rooms that were previously cached by
[`resolveRoomsInfo`](#createClientResolveRoomsInfo).
It can be used when updating a room’s name for example, to instantly refresh the
room info everywhere without having to perform a page reload.
```tsx
// Invalidate all rooms
client.resolvers.invalidateRoomsInfo();
// Only invalidate "room-0" and "room-1"
client.resolvers.invalidateRoomsInfo(["room-0", "room-1"]);
```
### invalidateGroupsInfo
`client.resolvers.invalidateGroupsInfo` can be used to invalidate some or all
groups that were previously cached by
[`resolveGroupsInfo`](#createClientResolveGroupsInfo).
It can be used when updating a group’s name for example, to instantly refresh
the group info everywhere without having to perform a page reload.
```tsx
// Invalidate all groups
client.resolvers.invalidateGroupsInfo();
// Only invalidate "group-0" and "group-1"
client.resolvers.invalidateGroupsInfo(["group-0", "group-1"]);
```
### invalidateMentionSuggestions
`client.resolvers.invalidateMentionSuggestions` can be used to invalidate all
mention suggestions that were previously cached by
[`resolveMentionSuggestions`](#createClientResolveMentionSuggestions).
It can be used when updating a room’s list of users for example, to prevent
creating out-of-date mentions without having to perform a page reload.
```tsx
// Invalidate all mention suggestions
client.resolvers.invalidateMentionSuggestions();
```
## Utilities
### getMentionsFromCommentBody [#get-mentions-from-comment-body]
Returns an array of mentions from a `CommentBody` (found under `comment.body`).
```ts
import { getMentionsFromCommentBody } from "@liveblocks/client";
const mentions = getMentionsFromCommentBody(comment.body);
```
An optional second argument can be used to filter the returned mentions. By
default, if it’s not provided, all mentions are returned, including future
mention kinds (e.g. group mentions in the future).
```tsx
// All mentions (same as `getMentionsFromCommentBody(commentBody)`)
getMentionsFromCommentBody(commentBody);
// Only user mentions with an ID of "123"
getMentionsFromCommentBody(
commentBody,
(mention) => mention.kind === "user" && mention.id === "123"
);
// Only mentions with an ID which starts with "prefix:"
getMentionsFromCommentBody(commentBody, (mention) => (
mention.id.startsWith("prefix:")
);
```
Here’s an example with a custom `CommentBody`.
```ts
import { CommentBody, getMentionsFromCommentBody } from "@liveblocks/client";
// Create a custom `CommentBody`
const commentBody: CommentBody = {
version: 1,
content: [
{
type: "paragraph",
children: [
{ text: "Hello " },
{ type: "mention", id: "chris@example.com" },
],
},
],
};
// Get the mentions inside the comment’s body
const mentions = getMentionsFromCommentBody(commentBody);
// [{ kind: "user", id: "chris@example.com" }]
console.log(mentions);
```
If you’d like to use this on the server side, it’s also available from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node#get-mentions-from-comment-body).
### stringifyCommentBody [#stringify-comment-body]
Used to convert a `CommentBody` (found under `comment.body`) into either a plain
string, Markdown, HTML, or a custom format.
```ts
import { stringifyCommentBody } from "@liveblocks/client";
const stringComment = await stringifyCommentBody(comment.body);
// "Hello marc@example.com from https://liveblocks.io"
console.log(stringComment);
```
A number of options are available.
```ts
import { stringifyCommentBody } from "@liveblocks/client";
const stringComment = await stringifyCommentBody(comment.body, {
// Optional, convert to specific format, "plain" (default) | "markdown" | "html"
format: "markdown",
// Optional, supply a separator to be used between paragraphs
separator: `\n\n`,
// Optional, override any elements in the CommentBody with a custom string
elements: {
// Optional, override the `paragraph` element
paragraph: ({ element, children }) => `
${children}
`,
// Optional, override the `text` element
text: ({ element }) =>
element.bold ? `${element.text}` : `${element.text}`,
// Optional, override the `link` element
link: ({ element, href }) =>
`${element.url}`,
// Optional, override the `mention` element.
// `user` and `group` are the optional data returned from `resolveUsers` and `resolveGroupsInfo`
mention: ({ element, user, group }) =>
`${
element.id
}`,
},
// Optional, get your user’s names and info from their ID to be displayed in mentions
async resolveUsers({ userIds }) {
const usersData = await __getUsersFromDB__(userIds);
return usersData.map((userData) => ({
// Name is inserted into the output instead of a user’s ID
name: userData.name,
// Custom formatting in `elements.mention` allows custom properties to be used
profileUrl: userData.profileUrl,
}));
},
// Optional, get your group’s names and info from their ID to be displayed in mentions
async resolveGroupsInfo({ groupIds }) {
const groupsData = await __getGroupsFromDB__(groupIds);
return groupsData.map((groupData) => ({
// Name is inserted into the output instead of a group’s ID
name: groupData.name,
// Custom formatting in `elements.mention` allows custom properties to be used
settingsUrl: groupData.settingsUrl,
}));
},
});
```
If you’d like to use this on the server side, it’s also available from
[`@liveblocks/node`](/docs/api-reference/liveblocks-node#stringify-comment-body).
#### Formatting examples
Here are a number of different formatting examples derived from the same
`CommentBody`.
```ts
// "Hello marc@example.com from https://liveblocks.io"
await stringifyCommentBody(comment.body);
// "Hello @Marc from https://liveblocks.io"
await stringifyCommentBody(comment.body, {
resolveUsers({ userIds }) {
return [{ name: "Marc" }];
},
});
// "**Hello** @Marc from [https://liveblocks.io](https://liveblocks.io)"
await stringifyCommentBody(comment.body, {
format: "markdown",
resolveUsers() {
return [{ name: "Marc" }];
},
});
// "Hello@Marc from
// https://liveblocks.io"
await stringifyCommentBody(comment.body, {
format: "html",
resolveUsers() {
return [{ name: "Marc" }];
},
});
// "Hello@Marc from
// https://liveblocks.io"
await stringifyCommentBody(comment.body, {
format: "html",
mention: ({ element, user }) =>
`${user.name}`,
resolveUsers() {
return [{ name: "Marc", profileUrl: "https://example.com" }];
},
});
```
## TypeScript
### Typing your data
It’s possible to have automatic types flow through your application by defining
a global `Liveblocks` interface. We recommend doing this in a
`liveblocks.config.ts` file in the root of your app, so it’s easy to keep track
of your types. Each type (`Presence`, `Storage`, etc.), is optional, but it’s
recommended to make use of them.
```ts file="liveblocks.config.ts"
declare global {
interface Liveblocks {
// Each user’s Presence
Presence: {};
// The Storage tree for the room
Storage: {};
UserMeta: {
id: string;
// Custom user info set when authenticating with a secret key
info: {};
};
// Custom events
RoomEvent: {};
// Custom metadata set on threads
ThreadMetadata: {};
// Custom 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
[`toimmutable`]: /docs/api-reference/liveblocks-client#LiveObject.toImmutable
[`node-fetch`]: https://npmjs.com/package/node-fetch
[`room.broadcastevent`]:
/docs/api-reference/liveblocks-client#Room.broadcastEvent
[`room.getstorage`]: /docs/api-reference/liveblocks-client#Room.getStorage
[`room.reconnect`]: /docs/api-reference/liveblocks-client#Room.reconnect
[`room.getself`]: /docs/api-reference/liveblocks-client#Room.getSelf
[`room.getothers`]: /docs/api-reference/liveblocks-client#Room.getOthers
[`room.getstorage`]: /docs/api-reference/liveblocks-client#Room.getStorage
[`room.getsubscriptionsettings`]:
/docs/api-reference/liveblocks-client#Room.getSubscriptionSettings
[`room.updatesubscriptionsettings`]:
/docs/api-reference/liveblocks-client#Room.updateSubscriptionSettings
[`room.history`]: /docs/api-reference/liveblocks-client#Room.history
[`room.subscribe("event")`]:
/docs/api-reference/liveblocks-client#Room.subscribe.event
[`room.subscribe("status")`]:
/docs/api-reference/liveblocks-client#Room.subscribe.status
[`room.subscribe("lost-connection")`]:
/docs/api-reference/liveblocks-client#Room.subscribe.lost-connection
[`room.subscribe("storage-status")`]:
/docs/api-reference/liveblocks-client#Room.subscribe.storage-status
[`room.updatepresence`]:
/docs/api-reference/liveblocks-client#Room.updatePresence
[`websocket`]: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
[`ws`]: https://www.npmjs.com/package/ws
[connection status example]:
https://liveblocks.io/examples/connection-status/nextjs
[3.4]: https://github.com/liveblocks/liveblocks/releases/tag/v3.4.0
---
meta:
title: "@liveblocks/emails"
parentTitle: "API Reference"
description: "API Reference for the @liveblocks/emails package"
alwaysShowAllNavigationLevels: false
---
`@liveblocks/emails` provides a set of functions that simplifies sending styled
emails with [Notifications](/docs/ready-made-features/notifications) and
[webhooks](/docs/platform/webhooks). This library is only intended for use in
your Node.js back end.
## Requirements
`@liveblocks/emails` requires the
[`@liveblocks/node`](/docs/api-reference/liveblocks-node) package to be
installed and for [`react`](https://react.dev/) to be a peer dependency in your
project.
## Setup
This package exposes functions that enable easy creation of styled emails with
React and HTML. Each method is designed to be used with our
[webhooks](/docs/platform/webhooks) which means you must
[set them up](/docs/guides/how-to-test-webhooks-on-localhost) first. Webhooks
require an API endpoint in your application, and this is typically what they
will look like.
```tsx title="Next.js route handler for webhooks"
import { isThreadNotificationEvent, WebhookHandler } from "@liveblocks/node";
import { Liveblocks } from "@liveblocks/node";
// +++
import { prepareThreadNotificationEmailAsReact } from "@liveblocks/emails";
// +++
const liveblocks = new Liveblocks({
secret: "{{SECRET_KEY}}",
});
const webhookHandler = new WebhookHandler(
process.env.LIVEBLOCKS_WEBHOOK_SECRET_KEY as string
);
export async function POST(request: Request) {
const body = await request.json();
const headers = request.headers;
// Verify if this is a real webhook request
let event;
try {
event = webhookHandler.verifyRequest({
headers: headers,
rawBody: JSON.stringify(body),
});
} catch (err) {
console.error(err);
return new Response("Could not verify webhook call", { status: 400 });
}
// +++
// Using `@liveblocks/emails` to create an email
if (isThreadNotificationEvent(event)) {
const emailData = await prepareThreadNotificationEmailAsReact(
liveblocks,
event
);
if (emailData.type === "unreadMention") {
const email = (
@{emailData.comment.author.id} at {emailData.comment.createdAt}
{emailData.comment.body}
);
// Send unread mention email
// ...
}
}
// +++
return new Response(null, { status: 200 });
}
```
**We’ll only show the highlighted part below**, as it’s assumed you’ve set this
already, and your file contains `liveblocks` and `event`.
### End-to-end guides
We have two guides that take you through every step of setting up your email
notifications, including setting up webhooks:
- [How to send email notifications of unread comments](/docs/guides/how-to-send-email-notifications-of-unread-comments).
- [How to send email notifications for unread text editor mentions ](/docs/guides/how-to-send-email-notifications-for-unread-text-editor-mentions).
### Ready-made email templates
We have a number of examples that show you how to set up emails with your
Comments or Text Editor application. Each [Resend](https://resend.com) example
has full ready-made email templates inside, which are a great starting point for
your application.
- [Comments + Resend](/examples/comments-emails/nextjs-comments-emails-resend).
- [Comments + SendGrid](/examples/comments-emails/nextjs-comments-emails-sendgrid).
- [Text Editor/Tiptap + Resend](/examples/collaborative-text-editor-emails/nextjs-tiptap-emails-resend).
- [Text Editor/Lexical + Resend](/examples/collaborative-text-editor-emails/nextjs-lexical-emails-resend).
## Thread notification emails [#thread-notification-emails]
These functions help you create emails to notify users of _unread comments_ in
threads. They fetch each relevant comment, filtering out any that have already
been read, and help you style each comment’s body with either
[React](#prepare-thread-notification-email-as-react) or
[HTML](#prepare-thread-notification-email-as-html).
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).
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.
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.
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,
// ...
},
},
});
```
This method has a number of options, allowing for custom metadata and a creation
date for the comment.
```ts
const thread = await liveblocks.createThread({
roomId: "my-room-id",
data: {
// Optional, custom metadata properties
metadata: {
color: "blue",
page: 3,
pinned: true,
},
// Data for the first comment in the thread
comment: {
// The ID of the user that created the comment
userId: "florent@example.com",
// Optional, when the comment was created.
createdAt: new Date(),
// Optional, custom comment metadata
metadata: {
tag: "important",
spam: false,
},
// The comment's body, uses the `CommentBody` type
body: {
version: 1,
content: [
/* The comment's body text goes here, see above */
],
},
},
},
});
// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```
#### Liveblocks.getThread [#get-rooms-roomId-threads-threadId]
Returns a thread. Throws an error if the room or thread isn’t found. This is a
wrapper around the
[Get Thread API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId)
and returns the same response.
```ts
const thread = await liveblocks.getThread({
roomId: "my-room-id",
threadId: "th_d75sF3...",
});
// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```
#### Liveblocks.editThreadMetadata [#post-rooms-roomId-threads-threadId-metadata]
Updates the metadata of a specific thread within a room. This method allows you
to modify the metadata of a thread, including user information and the date of
the last update. Throws an error if the room or thread isn’t found. This is a
wrapper around the
[Update Thread Metadata API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-metadata)
and returns the updated metadata.
```ts
const editedMetadata = await liveblocks.editThreadMetadata({
roomId: "my-room-id",
threadId: "th_d75sF3...",
data: {
metadata: {
color: "yellow",
},
userId: "marc@example.com",
updatedAt: new Date(), // Optional
},
});
// { color: "yellow", page: 3, pinned: true }
console.log(editedMetadata);
```
Metadata can be a `string`, `number`, or `boolean`. You can also use `null` to
remove metadata from a thread. Here’s an example using every option.
```ts
const editedMetadata = await liveblocks.editThreadMetadata({
roomId: "my-room-id",
threadId: "th_d75sF3...",
data: {
// Custom metadata
metadata: {
// Metadata can be a string, number, or boolean
title: "My thread title",
page: 3,
pinned: true,
// Remove metadata with null
color: null,
},
// The ID of the user that updated the metadata
userId: "marc@example.com",
// Optional, the time the user updated the metadata
updatedAt: new Date(),
},
});
// { title: "My thread title", page: 3, pinned: true }
console.log(editedMetadata);
```
#### Liveblocks.editCommentMetadata [#post-rooms-roomId-threads-threadId-comments-commentId-metadata]
Updates the metadata of a specific comment within a thread. This method allows
you to modify the metadata of a comment, including user information and the date
of the last update. Throws an error if the room, thread, or comment isn’t found.
This is a wrapper around the
[Update Comment Metadata API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-metadata)
and returns the updated metadata.
```ts
const editedMetadata = await liveblocks.editCommentMetadata({
roomId: "my-room-id",
threadId: "th_d75sF3...",
commentId: "cm_agH76a...",
data: {
metadata: {
spam: false,
},
userId: "stacy@example.com",
updatedAt: new Date(), // Optional
},
});
// { spam: false }
console.log(editedMetadata);
```
Metadata can be a `string`, `number`, or `boolean`. You can also use `null` to
remove metadata from a comment. Here’s an example using every option.
```ts
const editedMetadata = await liveblocks.editCommentMetadata({
roomId: "my-room-id",
threadId: "th_d75sF3...",
commentId: "cm_agH76a...",
data: {
// Custom metadata
metadata: {
// Metadata can be a string, number, or boolean
tag: "important",
priority: 2,
spam: true,
// Remove metadata with null
assignedTo: null,
},
// The ID of the user that updated the metadata
userId: "stacy@example.com",
// Optional, the time the user updated the metadata
updatedAt: new Date(),
},
});
// { tag: "important", priority: 2, flagged: true }
console.log(editedMetadata);
```
#### Liveblocks.markThreadAsResolved [#post-rooms-roomId-threads-threadId-mark-as-resolved]
Marks a thread as resolved, which means it sets the `resolved` property on the
specified thread to `true`. Takes a `userId`, which is the ID of the user that
resolved the thread. Throws an error if the room or thread isn’t found. This is
a wrapper around the
[Mark Thread As Resolved API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-resolved)
and returns the same response.
```ts
const thread = await liveblocks.markThreadAsResolved({
roomId: "my-room-id",
threadId: "th_d75sF3...",
data: {
userId: "steven@example.com",
},
});
// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```
#### Liveblocks.markThreadAsUnresolved [#post-rooms-roomId-threads-threadId-mark-as-unresolved]
Marks a thread as unresolved, which means it sets the `resolved` property on the
specified thread to `false`. Takes a `userId`, which is the ID of the user that
unresolved the thread. Throws an error if the room or thread isn’t found. This
is a wrapper around the
[Mark Thread As Unresolved API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-unresolved)
and returns the same response.
```ts
const thread = await liveblocks.markThreadAsUnresolved({
roomId: "my-room-id",
threadId: "th_d75sF3...",
data: {
userId: "steven@example.com",
},
});
// { type: "thread", id: "th_d75sF3...", ... }
console.log(thread);
```
#### Liveblocks.deleteThread [#delete-rooms-roomId-threads-threadId]
Deletes a thread. Throws an error if the room or thread isn’t found. This is a
wrapper around the
[Delete Thread API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-threads-threadId)
and returns no response.
```ts
await liveblocks.deleteThread({
roomId: "my-room-id",
threadId: "th_d75sF3...",
});
```
#### Liveblocks.subscribeToThread [#post-rooms-roomId-threads-threadId-subscribe]
Subscribes a user to a thread, meaning they will receive inbox notifications
when new comments are posted. Throws an error if the room or thread isn’t found.
This is a wrapper around the
[Subscribe To Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-subscribe)
and returns the same response.
```ts
const subscription = await liveblocks.subscribeToThread({
roomId: "my-room-id",
threadId: "th_d75sF3...",
data: {
userId: "steven@example.com",
},
});
// { kind: "thread", subjectId: "th_d75sF3...", ... }
console.log(subscription);
```
Subscribing will replace any existing subscription for the current thread
[set at room-level](#post-rooms-roomId-users-userId-subscription-settings). This
value can also be overridden by a room-level call that is run afterwards.
```ts
const roomId = "my-room-id";
const userId = "steven@example.com";
// 1. Disables notifications for all threads
await liveblocks.updateRoomSubscriptionSettings({
roomId,
userId,
data: {
threads: "none",
},
});
// 2. Enables notifications just for this thread, "th_d75sF3..."
await liveblocks.subscribeToThread({
roomId,
threadId: "th_d75sF3...",
data: { userId },
});
// 3. Disables notifications for all threads, including "th_d75sF3..."
await liveblocks.updateRoomSubscriptionSettings({
roomId,
userId,
data: {
threads: "none",
},
});
```
#### Liveblocks.unsubscribeFromThread [#post-rooms-roomId-threads-threadId-unsubscribe]
Unsubscribes a user from a thread, meaning they will no longer receive inbox
notifications when new comments are posted. Throws an error if the room or
thread isn’t found. This is a wrapper around the
[Unsubscribe From Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-unsubscribe)
and returns the same response.
```ts
await liveblocks.unsubscribeFromThread({
roomId: "my-room-id",
threadId: "th_d75sF3...",
data: {
userId: "steven@example.com",
},
});
```
Unsubscribing will replace any existing subscription for the current thread
[set at room-level](#post-rooms-roomId-users-userId-subscription-settings). This
value can also be overridden by a room-level call that is run afterwards.
```ts
const roomId = "my-room-id";
const userId = "steven@example.com";
// 1. Enables notifications for all thread activity
await liveblocks.updateRoomSubscriptionSettings({
roomId,
userId,
data: {
threads: "all",
},
});
// 2. Disables notifications just for this thread, "th_d75sF3..."
await liveblocks.unsubscribeFromThread({
roomId,
threadId: "th_d75sF3...",
data: { userId },
});
// 3. Enables notifications for all thread activity, including "th_d75sF3..."
await liveblocks.updateRoomSubscriptionSettings({
roomId,
userId,
data: {
threads: "none",
},
});
```
#### Liveblocks.getThreadSubscriptions [#get-rooms-roomId-threads-threadId-subscriptions]
Gets a thread’s subscriptions, returning a list of users that will receive
notifications when new comments are posted. Throws an error if the room or
thread isn’t found. This is a wrapper around the
[Get Thread Subscriptions API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId-subscriptions)
and returns the same response.
```ts
const { data: subscriptions } = await liveblocks.getThreadSubscriptions({
roomId: "my-room-id",
threadId: "th_d75sF3...",
});
// [{ kind: "thread", subjectId: "th_d75sF3...", userId: "steven@example.com", ... }, ...]
console.log(subscriptions);
```
#### Liveblocks.createComment [#post-rooms-roomId-threads-threadId-comments]
Creates a new comment in a specific thread within a room. This method allows
users to add comments to a conversation thread, specifying the user who made the
comment and the content of the comment. This method is a wrapper around the
[Create Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments)
and returns the new comment.
```ts
const comment = await liveblocks.createComment({
roomId: "my-room-id",
threadId: "th_d75sF3...",
data: {
body: {
version: 1,
content: [
/* The comment's body text goes here, see below */
],
},
userId: "pierre@example.com",
createdAt: new Date(), // Optional
},
});
```
A comment’s body is an array of paragraphs, each containing child nodes. Here’s
an example of how to construct a comment’s body, which can be submitted under
`data.body`.
```tsx highlight="3-11,19"
import { CommentBody } from "@liveblocks/node";
const body: CommentBody = {
version: 1,
content: [
{
type: "paragraph",
children: [{ text: "Hello " }, { text: "world", bold: true }],
},
],
};
const comment = await liveblocks.createComment({
roomId: "my-room-id",
threadId: "th_d75sF3...",
data: {
// The comment's body, uses the `CommentBody` type
body,
// ...
},
});
```
This method has a number of options, including the option to add a custom
creation date and metadata to the comment.
```ts
const comment = await liveblocks.createComment({
roomId: "my-room-id",
threadId: "th_d75sF3...",
data: {
// The comment's body, uses the `CommentBody` type
body: {
version: 1,
content: [
/* The comment's body text goes here, see above */
],
},
// The ID of the user that created the comment
userId: "adrien@example.com",
// Optional, the time the comment was created
createdAt: new Date(),
// Optional, custom comment metadata
metadata: {
tag: "important",
reviewed: false,
},
},
});
```
#### Liveblocks.getComment [#get-rooms-roomId-threads-threadId-comments-commentId]
Returns a comment. Throws an error if the room, thread, or comment isn’t found.
This is a wrapper around the
[Get Comment API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-threads-threadId-comments-commentId)
and returns the same response.
```ts
const comment = await liveblocks.getComment({
roomId: "my-room-id",
threadId: "th_d75sF3...",
commentId: "cm_agH76a...",
});
// { type: "comment", threadId: "th_d75sF3...", ... }
console.log(comment);
```
#### Liveblocks.editComment [#post-rooms-roomId-threads-threadId-comments-commentId]
Edits an existing comment in a specific thread within a room. This method allows
users to update the content of their previously posted comments, with the option
to specify the time of the edit. Throws an error if the comment isn’t found.
This is a wrapper around the
[Edit Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId)
and returns the updated comment.
```ts
const editedComment = await liveblocks.editComment({
roomId: "my-room-id",
threadId: "th_d75sF3...",
commentId: "cm_agH76a...",
data: {
userId: "alicia@example.com",
body: {
version: 1,
content: [
/* The comment's body text goes here, see below */
],
},
// Optional, the time the comment was edited
editedAt: new Date(),
// Optional, custom comment metadata
metadata: {
tag: "important",
spam: false,
},
},
});
// { type: "comment", threadId: "th_d75sF3...", ... }
console.log(editedComment);
```
A comment’s body is an array of paragraphs, each containing child nodes. Here’s
an example of how to construct a comment’s body, which can be submitted under
`data.body`.
```tsx
import { CommentBody } from "@liveblocks/node";
// +++
const body: CommentBody = {
version: 1,
content: [
{
type: "paragraph",
children: [{ text: "Hello " }, { text: "world", bold: true }],
},
],
};
// +++
const editedComment = await liveblocks.editComment({
roomId: "my-room-id",
threadId: "th_d75sF3...",
commentId: "cm_agH76a...",
data: {
// The comment's body, uses the `CommentBody` type
// +++
body,
// +++
// ...
},
});
```
#### Liveblocks.deleteComment [#delete-rooms-roomId-threads-threadId-comments-commentId]
Deletes a specific comment from a thread within a room. If there are no
remaining comments in the thread, the thread is also deleted. This method throws
an error if the comment isn’t found. This is a wrapper around the
[Delete Comment API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId)
and returns no response.
```ts
await liveblocks.deleteComment({
roomId: "my-room-id",
threadId: "th_d75sF3...",
commentId: "cm_agH76a...",
});
```
#### Liveblocks.addCommentReaction [#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction]
Adds a reaction to a specific comment in a thread within a room. Throws an error
if the comment isn’t found or if the user has already added the same reaction on
the comment. This is a wrapper around the
[Add Comment Reaction API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction)
and returns the new reaction.
```ts
const reaction = await liveblocks.addCommentReaction({
roomId: "my-room-id",
threadId: "th_d75sF3...",
commentId: "cm_agH76a...",
data: {
emoji: "👨👩👧",
userId: "guillaume@example.com",
createdAt: new Date(), // Optional, the time the reaction was added
},
});
// { emoji: "👨👩👧", userId "guillaume@example.com", ... }
console.log(reaction);
```
#### Liveblocks.removeCommentReaction [#post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction]
Removes a reaction from a specific comment in a thread within a room. Throws an
error if the comment reaction isn’t found. This is a wrapper around the
[Remove Comment Reaction API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction)
and returns no response.
```ts
await liveblocks.removeCommentReaction({
roomId: "my-room-id",
threadId: "th_d75sF3...",
commentId: "cm_agH76a...",
data: {
emoji: "👨👩👧",
userId: "steven@example.com",
removedAt: new Date(), // Optional, the time the reaction is to be removed
},
});
```
#### Liveblocks.getRoomSubscriptionSettings [#get-rooms-roomId-users-userId-subscription-settings]
Returns a user’s subscription settings for a specific room, specifying which
`thread` and `textMention` inbox notifications they are set to receive. This is
a wrapper around the
[Get Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#get-rooms-roomId-users-userId-subscription-settings).
```ts
const subscriptionSettings = await liveblocks.getRoomSubscriptionSettings({
roomId: "my-room-id",
userId: "steven@example.com",
});
// { threads: "all", textMentions: "mine" }
console.log(subscriptionSettings);
```
For `"threads"`, these are the three possible values:
- `"all"` Receive notifications for every activity in every thread.
- `"replies_and_mentions"` Receive notifications for mentions and threads you’re
participating in.
- `"none"` No notifications are received.
For `"textMentions"`, these are the two possible values:
- `"mine"` Receive notifications for mentions of you.
- `"none"` No notifications are received.
#### Liveblocks.updateRoomSubscriptionSettings [#post-rooms-roomId-users-userId-subscription-settings]
Updates a user’s subscription settings for a specific room, defining which
`thread` and `textMention` inbox notifications they will receive. This is a
wrapper around the
[Update Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-users-userId-subscription-settings).
```ts
const updatedSubscriptionSettings =
await liveblocks.updateRoomSubscriptionSettings({
roomId: "my-room-id",
userId: "steven@example.com",
data: {
threads: "replies_and_mentions",
textMentions: "mine",
},
});
// { threads: "replies_and_mentions", ... }
console.log(updatedSubscriptionSettings);
```
For `"threads"`, these are the three possible values that can be set:
- `"all"` Receive notifications for every activity in every thread.
- `"replies_and_mentions"` Receive notifications for mentions and threads you’re
participating in.
- `"none"` No notifications are received.
For `"textMentions"`, these are the two possible values that can be set:
- `"mine"` Receive notifications for mentions of you.
- `"none"` No notifications are received.
##### Replacing individual thread subscriptions
Subscribing will replace any
[existing thread subscriptions](#post-rooms-roomId-users-userId-subscription-settings)
in the current room. This value can also be overridden by a room-level call that
is run afterwards.
```ts
const roomId = "my-room-id";
const userId = "steven@example.com";
// 1. Enables notifications just for this thread, "th_d75sF3..."
await liveblocks.subscribeToThread({
roomId,
threadId: "th_d75sF3...",
data: { userId },
});
// 2. Disables notifications for all threads, including "th_d75sF3..."
await liveblocks.updateRoomSubscriptionSettings({
roomId,
userId,
data: {
threads: "none",
},
});
```
#### Liveblocks.deleteRoomSubscriptionSettings [#delete-rooms-roomId-users-userId-subscription-settings]
Deletes a user’s subscription settings for a specific room. This is a wrapper
around the
[Delete Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#delete-rooms-roomId-users-userId-subscription-settings).
```ts
await liveblocks.deleteRoomSubscriptionSettings({
roomId: "my-room-id",
userId: "steven@example.com",
});
```
#### Liveblocks.getUserRoomSubscriptionSettings [#get-users-userId-room-subscription-settings]
Returns a list of a user’s subscription settings for all rooms. This is a
wrapper around the
[Get User Room Subscription Settings API](/docs/api-reference/rest-api-endpoints#get-users-userId-room-subscription-settings).
```ts
const { data: subscriptionSettings, nextCursor } =
await liveblocks.getUserRoomSubscriptionSettings({
userId: "steven@example.com",
// Optional, filter for a specific 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",
data: {
role: "user",
content: "Updated content",
},
});
```
#### 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" }];
},
});
```
### WebhookHandler [#WebhookHandler]
Read the [Webhooks guide](/docs/platform/webhooks) to learn how to use them
within your product, allowing you to react to Liveblocks events as they happen.
The `WebhookHandler` class is a helper to handle webhook requests from
Liveblocks.
It’s initialized with a signing secret that you can find in your project’s
webhook page.
```js
const webhookHandler = new WebhookHandler(process.env.WEBHOOK_SECRET);
```
#### verifyRequest [#verifyRequest]
Verifies the request and returns the event. Note that `rawBody` takes the body
as a `string`.
```js
const event = webhookHandler.verifyRequest({
headers: req.headers,
rawBody: req.body,
});
```
Some frameworks parse request bodies into objects, which means using
`JSON.stringify` may be necessary.
```js highlight="3"
const event = webhookHandler.verifyRequest({
headers: req.headers,
rawBody: JSON.stringify(req.body),
});
```
##### Example using Next.js [#webhook-example]
```js
import { WebhookHandler } from "@liveblocks/node";
// Will fail if not properly initialized with a secret
// Obtained from the Webhooks section of your project dashboard
// https://liveblocks.io/dashboard
const webhookHandler = new WebhookHandler(process.env.WEBHOOK_SECRET);
export function POST(request) {
try {
const event = webhookHandler.verifyRequest({
headers: req.headers,
rawBody: JSON.stringify(req.body),
});
// Handle `WebhookEvent`
if (event.type === "storageUpdated") {
// Handle `StorageUpdatedEvent`
} else if (event.type === "userEntered") {
// Handle `UserEnteredEvent`
} else if (event.type === "userLeft") {
// Handle `UserLeftEvent`
}
} catch (error) {
console.error(error);
return new Response(error, { status: 400 });
}
}
```
### isThreadNotificationEvent [#isThreadNotificationEvent]
Type guard to check if a received webhook event is a
[`ThreadNotificationEvent`](/docs/platform/webhooks#Thread-notification) send
from Comments. Particularly helpful when creating
[thread notification emails](/docs/api-reference/liveblocks-emails#thread-notification-emails)
with webhooks.
```js
import { isThreadNotificationEvent } from "@liveblocks/node";
const event = webhookHandler.verifyRequest({
headers: req.headers,
rawBody: req.body,
});
// +++
if (isThreadNotificationEvent(event)) {
// Handle `ThreadNotificationEvent`
}
// +++
```
The check is made against the event type and event data kind.
### isTextMentionNotificationEvent [#isTextMentionNotificationEvent]
Type guard to check if a received webhook event is a
[`TextMentionNotificationEvent`](/docs/platform/webhooks#TextMention-notification)
sent from Text Editor. Particularly helpful for identifying text mentions when
sending email notifications.
```js
import { isTextMentionNotificationEvent } from "@liveblocks/node";
const event = webhookHandler.verifyRequest({
headers: req.headers,
rawBody: req.body,
});
// +++
if (isTextMentionNotificationEvent(event)) {
// Handle `TextMentionNotificationEvent`
}
// +++
```
### isCustomNotificationEvent [#isCustomNotificationEvent]
Type guard to check if a received webhook event is a
[`CustomNotificationEvent`](/docs/platform/webhooks#Custom-notification) sent
from
[`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger).
Particularly helpful for identifying custom notifications when sending email
notifications.
```js
import { isCustomNotificationEvent } from "@liveblocks/node";
const event = webhookHandler.verifyRequest({
headers: req.headers,
rawBody: req.body,
});
// +++
if (isCustomNotificationEvent(event)) {
// Handle `CustomNotificationEvent`
}
// +++
```
The check is made against the event type and event data kind.
[`room.getothers`]: /docs/api-reference/liveblocks-client#Room.getOthers
[Permissions REST API]: /docs/authentication/id-token
---
meta:
title: "@liveblocks/react-blocknote"
parentTitle: "API Reference"
description: "API Reference for the @liveblocks/react-blocknote package"
alwaysShowAllNavigationLevels: false
---
`@liveblocks/react-blocknote` provides you with a [React](https://react.dev/)
plugin that adds collaboration to any
[BlockNote rich-text editor](https://www.blocknotejs.org/). It also adds
realtime cursors, document persistence on the cloud, comments, and mentions. Use
[`@liveblocks/node-prosemirror`](/docs/api-reference/liveblocks-node-prosemirror)
for server-side editing.
## Setup
To set up your collaborative BlockNote editor, create an editor with
[`useCreateBlockNoteWithLiveblocks`](#useCreateBlockNoteWithLiveblocks) and pass
it into the
[`BlockNoteView`](https://www.blocknotejs.org/docs/editor-basics/setup#rendering-the-editor-with-blocknoteview)
component provided by `@blocknote/mantine`.
```tsx
import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";
function TextEditor() {
// +++
const editor = useCreateBlockNoteWithLiveblocks({});
// +++
return (
// +++
// +++
);
}
```
Liveblocks BlockNote components should be passed `editor` to enable them.
```tsx
import {
useCreateBlockNoteWithLiveblocks,
// +++
FloatingComposer,
// +++
} from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";
function TextEditor() {
const editor = useCreateBlockNoteWithLiveblocks({});
return (
// +++
// +++
);
}
```
Learn more in our [get started guides](/docs/get-started/text-editor/blocknote).
## Default components
### FloatingComposer
Displays a [`Composer`][] near the current BlockNote selection, allowing you to
create threads.
```tsx highlight="3"
```
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"
```
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"
```
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 (
);
}
```
#### Offline support [@badge=experimental]
It’s possible to enable offline support in your editor with an experimental
option. This means that once a document has been opened, it’s saved locally on
the browser, and can be shown instantly without a loading screen. As soon as
Liveblocks connects, any remote changes will be synchronized, without any load
spinner. Enable this by passing a `offlineSupport_experimental` value.
```tsx
import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";
function TextEditor() {
const editor = useCreateBlockNoteWithLiveblocks(
{},
{
// +++
offlineSupport_experimental: true,
// +++
}
);
// ...
}
```
To make sure that your editor loads instantly, you must structure your app
carefully to avoid any Liveblocks hooks and [`ClientSideSuspense`][] components
from triggering a loading screen. For example, if you’re displaying threads in
your editor with [`useThreads`][], you must place this inside a separate
component and wrap it in [`ClientSideSuspense`][].
```tsx
"use client";
import { ClientSideSuspense, useThreads } from "@liveblocks/react/suspense";
import {
useCreateBlockNoteWithLiveblocks,
AnchoredThreads,
FloatingComposer,
} from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";
import { BlockNoteEditor } from "@blocknote/core";
export function BlockNoteEditor() {
const editor = useCreateBlockNoteWithLiveblocks(
{},
{
// +++
offlineSupport_experimental: true,
// +++
}
);
return (
<>
// +++
// +++
>
);
}
// +++
function Threads({ editor }: { editor: BlockNoteEditor }) {
const { threads } = useThreads();
return ;
}
// +++
```
### useIsEditorReady
Used to check if the editor content has been loaded or not, helpful for
displaying a loading skeleton.
```ts
import { useIsEditorReady } from "@liveblocks/react-blocknote";
const status = useIsEditorReady();
```
Here's how it can be used in the context of your editor.
```tsx
import {
useCreateBlockNoteWithLiveblocks,
useIsEditorReady,
} from "@liveblocks/react-blocknote";
import { BlockNoteView } from "@blocknote/mantine";
function TextEditor() {
const editor = useCreateBlockNoteWithLiveblocks({});
// +++
const ready = useIsEditorReady();
// +++
return (
// +++
{!ready ?
Loading...
: }
// +++
);
}
```
## Stylesheets
React BlockNote comes with default styles, and these can be imported into the
root of your app or directly into a CSS file with `@import`. Note that you must
also install and import a stylesheet from
[`@liveblocks/react-ui`](/docs/api-reference/liveblocks-react-ui) to use these
styles.
```tsx
import "@liveblocks/react-ui/styles.css";
import "@liveblocks/react-blocknote/styles.css";
```
### Customizing your styles
Adding dark mode and customizing your styles is part of `@liveblocks/react-ui`,
learn how to do this under
[styling and customization](/docs/api-reference/liveblocks-react-ui#Styling-and-customization).
[`Thread`]: /docs/api-reference/liveblocks-react-ui#Thread
[`Composer`]: /docs/api-reference/liveblocks-react-ui#Composer
[`useThreads`]: /docs/api-reference/liveblocks-react#useThreads
[`Icon`]: /docs/api-reference/liveblocks-react-ui#Icon
[`FloatingToolbar`]: #FloatingToolbar
[`FloatingComposer`]: #FloatingComposer
[`FloatingThreads`]: #FloatingThreads
[`AnchoredThreads`]: #AnchoredThreads
[`ClientSideSuspense`]: /docs/api-reference/liveblocks-react#ClientSideSuspense
---
meta:
title: "@liveblocks/react-lexical"
parentTitle: "API Reference"
description: "API Reference for the @liveblocks/react-lexical package"
alwaysShowAllNavigationLevels: false
---
`@liveblocks/react-lexical` provides you with a [React](https://react.dev/)
plugin that adds collaboration to any [Lexical](https://lexical.dev/) text
editor. It also adds realtime cursors, document persistence on the cloud,
comments, and mentions. Read our
[get started guides](/docs/get-started/text-editor/lexical) to learn more.
## Setup
To set up your collaborative Lexical editor, you must use
[`LiveblocksPlugin`](#LiveblocksPlugin) and
[`liveblocksConfig`](#liveblocksConfig).
### LiveblocksPlugin
Liveblocks plugin for Lexical that adds collaboration to your editor.
```tsx highlight="2"
```
{/* TODO: Image */}
`LiveblocksPlugin` should always be nested inside [`LexicalComposer`][], and
each [Lexical default component](#Default-components) you’re using should be
placed inside the plugin.
```tsx
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
// +++
import { liveblocksConfig, LiveblocksPlugin } from "@liveblocks/react-lexical";
// +++
const initialConfig = liveblocksConfig({
namespace: "MyEditor",
theme: {},
nodes: [],
onError: (err) => console.error(err),
});
function Editor() {
return (
// +++
// +++
}
placeholder={
Enter some text...
}
ErrorBoundary={LexicalErrorBoundary}
/>
);
}
```
Annotations associated with resolved threads are hidden by default on the
editor.
Learn more in our [get started guides](/docs/get-started/text-editor/lexical).
### liveblocksConfig
Function that takes a Lexical editor config and modifies it to add the necessary
`nodes` and `theme` to make [`LiveblocksPlugin`][] works correctly.
```tsx
import { liveblocksConfig } from "@liveblocks/react-lexical";
const initialConfig = liveblocksConfig({
namespace: "MyEditor",
theme: {},
nodes: [],
onError: (err) => console.error(err),
});
```
The config created by `liveblocksConfig` should be passed to `initialConfig` in
[`LexicalComposer`][].
```tsx highlight="7-12,16"
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
import { liveblocksConfig, LiveblocksPlugin } from "@liveblocks/react-lexical";
const initialConfig = liveblocksConfig({
namespace: "MyEditor",
theme: {},
nodes: [],
onError: (err) => console.error(err),
});
function Editor() {
return (
}
placeholder={
Enter some text...
}
ErrorBoundary={LexicalErrorBoundary}
/>
);
}
```
Note that `liveblocksConfig` sets `editorState` to `null` because
`LiveblocksPlugin` is responsible for initializing it on the server.
## Default components
### Toolbar
Displays a toolbar, allowing you to change the styles of selected text. You can
add content [before or after](#toolbar-extending-the-defaults), or the toolbar’s
options can be [customized](#creating-a-custom-toolbar). A
[floating toolbar](#FloatingToolbar) also exists.
```tsx
// +++
// +++
```
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: