# Return `groups`
[{ name: "Engineering", avatar: "https://example.com/engineering.png" }, { name: "Design", avatar: "https://example.com/design.png" }]
```

Add a property named
[`resolveGroupsInfo`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo)
to your
[`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
where you can return this information.

```tsx
<LiveblocksProvider
  authEndpoint="/api/liveblocks-auth"
  // +++
  resolveGroupsInfo={async ({ groupIds }) => {
    // ["engineering", ...]
    console.log(groupIds);

    // Get groups from your back-end
    const groups = await __fetchGroups__(groupIds);

    // [{ name: "Engineering", avatar: "https://example.com/engineering.png" }, ...]
    console.log(groups);

    // Return a list of groups
    return groups;
  }}
  // +++
>
```

The `name` and `avatar` are handled by the default components, but you can also
return custom metadata here. For example, each group may have a `color`
property. You can retrieve these properties in your app with
[`useGroupInfo`](/docs/api-reference/liveblocks-react#useGroupInfo).

```tsx
function Component() {
  const { group } = useGroupInfo("engineering");

  // { color: "blue", name: "Engineering", avatar: "https://example.com/engineering.png" }
  console.log(group);

  // ...
}
```

---
meta:
  title: "Multiplayer"
  parentTitle: "Ready-made features"
  description:
    "Add realtime collaboration to any surface in your product—from text editors
    to whiteboards, forms, and beyond."
---

Enable realtime collaboration across any surface in your product—just like in
Figma, Notion, or Linear. With Liveblocks Multiplayer, you can make anything
multiplayer: rich text editors, whiteboards, flowcharts, forms, spreadsheets, or
fully custom interfaces.

Liveblocks provides a permanent, realtime data store for your collaborative
state—so you don’t need to synchronize, scale, or maintain any infrastructure
yourself. All the hard parts of multiplayer—conflict resolution, presence,
history, and storage—are handled for you.

Depending on what you’re building, you can use purpose-built SDKs for rich text
editors (like Tiptap, Lexical, or BlockNote), or more general packages like
`@liveblocks/client`, `@liveblocks/react`, `@liveblocks/zustand`, or
`@liveblocks/redux`.

## Text editor integrations

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="Tiptap"
    href="/docs/ready-made-features/multiplayer/text-editor/tiptap"
    description="Add multiplayer editing to Tiptap"
    visual={<DocsTiptapIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="BlockNote"
    href="/docs/ready-made-features/multiplayer/text-editor/blocknote"
    description="Add multiplayer editing to BlockNote"
    visual={
      <DocsBlocknotejsIcon className="fill-product-icon-brand h-auto w-6" />
    }
  />
  <DocsCard
    type="technology"
    title="Lexical"
    href="/docs/ready-made-features/multiplayer/text-editor/lexical"
    description="Add multiplayer editing to Lexical"
    visual={<DocsLexicalIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Sync engine integrations

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="Liveblocks Yjs"
    href="/docs/ready-made-features/multiplayer/sync-engine/liveblocks-yjs"
    description="Build custom multiplayer experiences"
    visual={<DocsYjsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Liveblocks Storage"
    href="/docs/ready-made-features/multiplayer/sync-engine/liveblocks-storage"
    description="Build custom multiplayer experiences"
    visual={<DocsStorageIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Multiplayer API Reference

### Text editor SDKs

Use these SDKs to add multiplayer editing to rich text editors.

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="Tiptap React"
    href="/docs/api-reference/liveblocks-react-tiptap"
    description="@liveblocks/react-tiptap"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="BlockNote React"
    href="/docs/api-reference/liveblocks-react-blocknote"
    description="@liveblocks/react-blocknote"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Lexical React"
    href="/docs/api-reference/liveblocks-react-lexical"
    description="@liveblocks/react-lexical"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="ProseMirror Node"
    href="/docs/api-reference/liveblocks-node-prosemirror"
    description="@liveblocks/node-prosemirror"
    visual={<DocsNodejsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Lexical Node"
    href="/docs/api-reference/liveblocks-node-lexical"
    description="@liveblocks/node-lexical"
    visual={<DocsNodejsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

### Sync engine SDKs

Use these SDKs to add multiplayer editing to whiteboards, forms, tables, or any
custom UI. These SDKs integrate directly with
[Liveblocks Storage](/docs/ready-made-features/multiplayer/sync-engine/liveblocks-storage)
and
[Liveblocks Yjs](/docs/ready-made-features/multiplayer/sync-engine/liveblocks-yjs).

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="React"
    href="/docs/api-reference/liveblocks-react"
    description="@liveblocks/react"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Client"
    href="/docs/api-reference/liveblocks-client"
    description="@liveblocks/client"
    visual={<DocsJavascriptIcon />}
  />
  <DocsCard
    type="technology"
    title="Zustand"
    href="/docs/api-reference/liveblocks-zustand"
    description="@liveblocks/zustand"
    visual={
      <img
        alt=""
        src="/assets/zustand.png"
        width={24}
        height={24}
        loading="lazy"
      />
    }
  />
  <DocsCard
    type="technology"
    title="Redux"
    href="/docs/api-reference/liveblocks-redux"
    description="@liveblocks/redux"
    visual={<DocsReduxIcon />}
  />
</ListGrid>

## Examples using Multiplayer

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative text editor",
      slug: "collaborative-text-editor/lexical",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Advanced collaborative text editor",
      slug: "collaborative-text-editor-advanced/nextjs-tiptap-advanced",
      image: "/images/examples/thumbnails/text-editor-advanced.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Linear-like issue tracker",
      slug: "linear-like-issue-tracker",
      image: "/images/examples/thumbnails/linear-like-issue-tracker.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI editor",
      slug: "notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Tldraw whiteboard",
      slug: "tldraw-whiteboard",
      image: "/images/examples/thumbnails/tldraw-whiteboard.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Sync engines"
  parentTitle: "Multiplayer"
  description:
    "Embed a custom collaborative experience into your application using
    Liveblocks Storage and Liveblocks Yjs."
---

Build a custom collaborative experience using Sync Datastore. This can include
anything from whiteboards, flowcharts, spreadsheets, and more. Liveblocks
permanently stores all sync engine data in each
[room](/docs/concepts/how-liveblocks-works#Rooms), handling scaling and
maintenance for you.

## Overview

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="Yjs"
    href="/docs/ready-made-features/multiplayer-editing/sync-engine/liveblocks-yjs"
    description="Sync engine optimized for text editors"
    visual={<DocsYjsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Storage"
    href="/docs/ready-made-features/multiplayer-editing/sync-engine/liveblocks-storage"
    description="Sync engine optimized for creative tools"
    visual={<DocsStorageIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Examples using Sync Datastore

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Tldraw whiteboard",
      slug: "tldraw-whiteboard",
      image: "/images/examples/thumbnails/tldraw-whiteboard.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Whiteboard",
      slug: "collaborative-whiteboard-advanced",
      image:
        "/images/examples/thumbnails/collaborative-whiteboard-advanced.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Flowchart",
      slug: "collaborative-flowchart/zustand-flowchart",
      image: "/images/examples/thumbnails/collaborative-flowchart.jpg",
      advanced: true,
    }}
    technologies={["zustand"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Liveblocks Storage"
  parentTitle: "Sync engine"
  description:
    "Liveblocks Storage is a realtime sync engine designed for multiplayer
    creative tools such as Figma, Pitch, and Spline."
---

Liveblocks Storage is a realtime sync engine designed for multiplayer creative
tools such as Figma, Pitch, and Spline. `LiveList`, `LiveMap`, and `LiveObject`
conflict-free data types can be used to build all sorts of multiplayer tools.
Liveblocks permanently stores Storage data in each
[room](/docs/concepts/how-liveblocks-works#Rooms), handling scaling and
maintenance for you.

## API Reference

### Presence

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="JavaScript"
    href="/docs/api-reference/liveblocks-client#Room.getPresence"
    description="@liveblocks/client"
    visual={<DocsJavascriptIcon />}
  />
  <DocsCard
    type="technology"
    title="React"
    href="/docs/api-reference/liveblocks-react#Presence"
    description="@liveblocks/react"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Redux"
    href="/docs/api-reference/liveblocks-redux#enhancer-option-presence-mapping"
    description="@liveblocks/redux"
    visual={<DocsReduxIcon />}
  />
  <DocsCard
    type="technology"
    title="Zustand"
    href="/docs/api-reference/liveblocks-zustand#middleware-option-presence-mapping"
    description="@liveblocks/zustand"
    visual={
      <img
        alt=""
        src="/assets/zustand.png"
        width={24}
        height={24}
        loading="lazy"
      />
    }
  />
</ListGrid>

### Broadcast

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="JavaScript"
    href="/docs/api-reference/liveblocks-client#Room.broadcastEvent"
    description="@liveblocks/client"
    visual={<DocsJavascriptIcon />}
  />
  <DocsCard
    type="technology"
    title="React"
    href="/docs/api-reference/liveblocks-react#Broadcast"
    description="@liveblocks/react"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Node.js"
    href="/docs/api-reference/liveblocks-node#post-broadcast-event"
    description="@liveblocks/node"
    visual={<DocsNodejsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="REST API"
    href="/docs/api-reference/rest-api-endpoints#post-broadcast-event"
    description="HTTP endpoints"
    visual={<DocsApiIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

### Storage

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="JavaScript"
    href="/docs/api-reference/liveblocks-client#Storage"
    description="@liveblocks/client"
    visual={<DocsJavascriptIcon />}
  />
  <DocsCard
    type="technology"
    title="React"
    href="/docs/api-reference/liveblocks-react#Storage"
    description="@liveblocks/react"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Redux"
    href="/docs/api-reference/liveblocks-redux#enhancer-option-storage-mapping"
    description="@liveblocks/redux"
    visual={<DocsReduxIcon />}
  />
  <DocsCard
    type="technology"
    title="Zustand"
    href="/docs/api-reference/liveblocks-zustand#middleware-option-storage-mapping"
    description="@liveblocks/zustand"
    visual={
      <img
        alt=""
        src="/assets/zustand.png"
        width={24}
        height={24}
        loading="lazy"
      />
    }
  />
  <DocsCard
    type="technology"
    title="Node.js"
    href="/docs/api-reference/liveblocks-node#Storage"
    description="@liveblocks/node"
    visual={<DocsNodejsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="REST API"
    href="/docs/api-reference/rest-api-endpoints#Storage"
    description="HTTP endpoints"
    visual={<DocsApiIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Examples using Liveblocks Storage

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Live Cursors Chat",
      slug: "live-cursors-chat",
      image: "/images/examples/thumbnails/live-cursors-chat.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Todo List",
      slug: "collaborative-todo-list",
      image: "/images/examples/thumbnails/collaborative-todo-list.jpg",
    }}
    technologies={["nextjs", "redux", "zustand", "javascript", "react-native"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Spreadsheet",
      slug: "collaborative-spreadsheet-advanced",
      image:
        "/images/examples/thumbnails/collaborative-spreadsheet-advanced.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Whiteboard",
      slug: "collaborative-whiteboard-advanced",
      image:
        "/images/examples/thumbnails/collaborative-whiteboard-advanced.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Flowchart",
      slug: "collaborative-flowchart/zustand-flowchart",
      image: "/images/examples/thumbnails/collaborative-flowchart.jpg",
      advanced: true,
    }}
    technologies={["zustand"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Liveblocks Yjs"
  parentTitle: "Sync engine"
  description:
    "Liveblocks Yjs is a realtime sync engine designed for collaborative text
    editors such as Notion and Google Docs."
---

Liveblocks Yjs is a realtime sync engine designed for building collaborative
text editors such as Notion and Google Docs. Liveblocks permanently stores Yjs
data in each [room](/docs/concepts/how-liveblocks-works#Rooms), handling scaling
and maintenance for you.

## Text editor integrations vs sync engines

If you’re using Tiptap, BlockNote, or Lexical, we recommend using
[Text Editor](/docs/ready-made-features/multiplayer-editing/text-editor)
instead. This is an alternative to Sync Datastore, with extra features
specifically created for those editors. Sync Datastore, powered by Liveblocks
Yjs and Liveblocks Storage, is intended for use in other editors, or for
completely custom solutions.

## API Reference

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="Yjs"
    href="/docs/api-reference/liveblocks-yjs"
    description="@liveblocks/yjs"
    visual={<DocsYjsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Node.js"
    href="/docs/api-reference/liveblocks-node#Yjs"
    description="@liveblocks/node"
    visual={<DocsNodejsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="REST API"
    href="/docs/api-reference/rest-api-endpoints#Yjs"
    description="HTTP endpoints"
    visual={<DocsApiIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Examples using Liveblocks Yjs

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative Text Editor (Slate)",
      slug: "collaborative-text-editor/nextjs-yjs-slate",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Text Editor (Quill)",
      slug: "collaborative-text-editor/nextjs-yjs-quill",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Collaborative Code Editor",
      slug: "collaborative-code-editor",
      image: "/images/examples/thumbnails/code-editor.jpg",
      advanced: true,
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Text editor"
  parentTitle: "Multiplayer"
  description:
    "Transform your existing text editor into a collaborative one with realtime
    synchronization using our ready-made integrations."
---

Transform your existing text editor into a collaborative one that feels like
Google Docs or Notion. Our ready-made integrations with Tiptap, Blocknote, and
Lexical handle all the realtime synchronization and storage for you, so you can
focus on building your application.

## Features

Our text editor integrations go beyond multiplayer editing to provide a complete
collaborative experience:

- **Inline comments & annotations**: Let users highlight text and leave
  comments, creating discussion threads right in the document.
- **Mentions and notifications**: Tag team members to notify them about
  important changes or discussions.
- **Version history**: Track changes over time and restore previous versions
  when needed.
- **Multiplayer undo/redo**: Each user has their own undo stack, preventing
  conflicts when multiple people edit simultaneously.
- **AI capabilities** (Tiptap only): Enhance your editor with AI-powered
  features through our built-in AI toolbar.

## Overview

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="Tiptap"
    href="/docs/ready-made-features/multiplayer-editing/text-editor/tiptap"
    description="Add collaboration to your Tiptap editor"
    visual={<DocsTiptapIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Blocknote"
    href="/docs/ready-made-features/multiplayer-editing/text-editor/blocknote"
    description="Add collaboration to your Blocknote editor"
    visual={
      <DocsBlocknotejsIcon className="fill-product-icon-brand h-auto w-6" />
    }
  />
  <DocsCard
    type="technology"
    title="Lexical"
    href="/docs/ready-made-features/multiplayer-editing/text-editor/lexical"
    description="Add collaboration to your Lexical editor"
    visual={<DocsLexicalIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Examples using text editor integrations

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Collaborative text editor",
      slug: "collaborative-text-editor/lexical",
      image: "/images/examples/thumbnails/text-editor.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Advanced collaborative text editor",
      slug: "collaborative-text-editor-advanced/nextjs-tiptap-advanced",
      image: "/images/examples/thumbnails/text-editor-advanced.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Linear-like issue tracker",
      slug: "linear-like-issue-tracker",
      image: "/images/examples/thumbnails/linear-like-issue-tracker.jpg",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Notion-like AI editor",
      slug: "notion-like-ai-editor",
      image: "/images/examples/thumbnails/notion-like-ai-editor.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "BlockNote"
  parentTitle: "Text editor"
  description: "Learn about using Multiplayer for BlockNote"
---

A quick overview of the packages for BlockNote.

## Concepts

Liveblocks BlockNote allows you add collaboration to any
[BlockNote](https://blocknotejs.org/) text editor, along with a number of
related features. Each
[collaborative room](/docs/concepts/how-liveblocks-works#Rooms) in your
application can store one document each, and these documents are persisted on
the cloud, visible on [your dashboard](/dashboard), and are integrated into
other Liveblocks products such as [Comments](/docs/ready-made-features/comments)
and [Notifications](/docs/ready-made-features/notifications).

### Permanent storage

All documents created with BlockNote are **permanently stored**. You don’t need
to sync, scale, or maintain any infrastructure.

### Users and mentions

Users and groups can be added to your document, and you can tag others inline.
You can also easily enable mention suggestions for both users and groups.

<Figure>
  <Image
    src="/assets/text-editor/mentions.png"
    alt="User mentions"
    width={1312}
    height={874}
  />
</Figure>

Users, groups, and mention suggestions can be added with
[`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers),
[`resolveGroupsInfo`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo),
and
[`resolveMentionSuggestions`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveMentionSuggestions).

```tsx highlight="6-16"
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      resolveUsers={async ({ userIds }) => {
        // Return user info from their `userIds`
        // ...
      }}
      resolveGroupsInfo={async ({ groupIds }) => {
        // Return group info from their `groupIds`
        // ...
      }}
      resolveMentionSuggestions={async ({ text, roomId }) => {
        // Return suggestions from the search `text`
        // Can return user IDs or mention objects for both users and groups
        // ...
      }}
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

### Realtime editing

Your document can be edited in real-time by multiple users at once. Each user
renders a cursor on screen that updates live as they move, select, and edit.

<Figure>
  <Image
    src="/assets/text-editor/cursors.png"
    alt="Real-time text cursors"
    width={1312}
    height={874}
  />
</Figure>

When [authenticating your users](/docs/authentication) with
[`prepareSession`](/docs/api-reference/liveblocks-node#access-tokens) or
[`identifyUser`](/docs/api-reference/liveblocks-node#id-tokens), pass `name` and
`color` properties to their `userInfo` to add their cursor information.

```js
userInfo: {
  name: "Marie",
  color: "#00ff00",
}
```

### Annotations and comments

Add [Comments](/docs/ready-made-features/comments) to your text editor, allowing
others to select words in the editor, and leave annotations. Each annotation
creates a [thread](/docs/ready-made-features/comments/concepts#Threads), and
inside each you can reply, use emoji reactions, mention others, and more.

<Figure>
  <Image
    src="/assets/text-editor/annotations.png"
    alt="Text editor annotations"
    width={1312}
    height={874}
  />
</Figure>

Add a floating Comments composer to your text editor using
[`FloatingComposer`](/docs/api-reference/liveblocks-react-blocknote#FloatingComposer).

```tsx
<FloatingComposer editor={editor} />
```

Add [`useThreads`](/docs/ready-made-features/comments/hooks#threads-hook)
alongside
[`FloatingThreads`](/docs/api-reference/liveblocks-react-blocknote#FloatingThreads)
and
[`AnchoredThreads`](/docs/api-reference/liveblocks-react-blocknote#AnchoredThreads)
to render your comments to the page.

```tsx
export function Threads({ editor }) {
  const { threads } = useThreads();

  return (
    <>
      <AnchoredThreads editor={editor} threads={threads} />
      <FloatingThreads editor={editor} threads={threads} />
    </>
  );
}
```

`FloatingThreads` displays floating threads below text highlights in the editor,
ideal for mobile, whereas `AnchoredThreads` displays threads vertically
alongside the editor which is great on desktop. To create a new thread use the
button in the BlockNote toolbar, or
[build a button yourself](/docs/api-reference/liveblocks-react-blocknote#Opening-the-composer).

### Notifications

Add a [Notifications](/docs/ready-made-features/notifications) UI to your
application, and automatically notify users when they’ve been mentioned.
Notifications also allows you to trigger sending emails, Slack, or any other
kind of notification, using our webhooks.

<Figure>
  <Image
    src="/assets/text-editor/notifications.png"
    alt="Text editor notifications"
    width={1312}
    height={874}
  />
</Figure>

Notifications is enabled by default, which means you just need to add our UI
components to keep your users notified. Follow our
[get started guides](/docs/get-started/notifications) to learn how to set up
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification).

```tsx
export function CollaborativeApp() {
  const { inboxNotifications } = useInboxNotifications();

  return (
    <InboxNotificationList>
      {inboxNotifications.map((inboxNotification) => (
        <InboxNotification
          key={inboxNotification.id}
          inboxNotification={inboxNotification}
        />
      ))}
    </InboxNotificationList>
  );
}
```

Inline mentions also trigger
[`TextMention`](/docs/platform/webhooks#TextMention-notification) notification
webhook events. Learn more about
[sending email notifications](/docs/ready-made-features/notifications/email-notifications).

### Multiple editors

It’s possible to use multiple BlockNote editors on a single page, and each
editor having its own collaboration and extensions. You can enable this by
giving each editor a unique `field` value.

```tsx
import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";

function TextEditor() {
  const editor = useCreateBlockNoteWithLiveblocks(
    {},
    {
      field: "editor-one",
    }
  );

  // ...
}
```

Learn more about
[using multiple editors](/docs/api-reference/liveblocks-react-blocknote#Multiple-editors).

### Offline support

Liveblocks BlockNote has an experimental option that enables offline support.
Offline support means that once a document has been opened, it’s saved locally
on the browser, and can be shown instantly without a loading screen. Enable this
by passing a `offlineSupport_experimental` value.

```tsx
import { useCreateBlockNoteWithLiveblocks } from "@liveblocks/react-blocknote";

function TextEditor() {
  const editor = useCreateBlockNoteWithLiveblocks(
    {},
    {
      offlineSupport_experimental: true,
    }
  );

  // ...
}
```

Learn more about
[offline support](/docs/api-reference/liveblocks-react-blocknote#Offline-support).

### Version History

Liveblocks BlockNote enables version history for your document. Versions can be
automatically created when enabled in your project settings, or manually created
using the
[REST API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-version).
These versions can be easily displayed to users using a few simple components.
This allows for a comprehensive version history feature in your collaborative
text editor.

To implement version history in your application, you can use the
`useHistoryVersions` hook along with the `HistoryVersionsList` and
`HistoryVersionPreview` components. Here's an example of how to set this up:

```tsx
import {
  useHistoryVersions,
  HistoryVersionSummaryList,
} from "@liveblocks/react";
import { HistoryVersionPreview } from "@liveblocks/react-blocknote";

function DocumentHistory() {
  const [selectedVersionId, setSelectedVersionId] = useState<string>();
  const { versions, isLoading } = useHistoryVersions();
  const selectedVersion = useMemo(
    () => versions?.find((version) => version.id === selectedVersionId),
    [selectedVersionId, versions]
  );

  if (isLoading) {
    return <div>Loading version history...</div>;
  }

  return (
    <>
      <div>
        {selectedVersion ? (
          <HistoryVersionPreview
            version={selectedVersion}
            className="h-full w-full"
            onVersionRestore={onVersionRestore}
          />
        ) : (
          <div>No version selected</div>
        )}
      </div>

      <div>
        <HistoryVersionSummaryList>
          {versions?.map((version) => (
            <HistoryVersionSummary
              onClick={() => {
                setSelectedVersionId(version.id);
              }}
              key={version.id}
              version={version}
              selected={version.id === selectedVersionId}
            />
          ))}
        </HistoryVersionSummaryList>
      </div>
    </>
  );
}
```

In this example, `useHistoryVersions` fetches the version history,
`HistoryVersionSummaryList` displays a list of available versions, and
`HistoryVersionPreview` shows a preview of the selected version. This setup
provides users with an interface to browse through the document's history and
restore different versions.

## BlockNote API Reference

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="BlockNote React"
    href="/docs/api-reference/liveblocks-react-blocknote"
    description="@liveblocks/react-blocknote"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="BlockNote/ProseMirror Node"
    href="/docs/api-reference/liveblocks-node-prosemirror"
    description="@liveblocks/node-prosemirror"
    visual={<DocsApiIcon />}
  />
</ListGrid>

---
meta:
  title: "Lexical"
  parentTitle: "Text editor"
  description: "Learn about using Multiplayer for Lexical"
---

A quick overview of the packages for Lexical.

## Concepts

Liveblocks Lexical allows you to add collaboration to any
[Lexical](https://lexical.dev/) text editor, along with a number of related
features. Each [collaborative room](/docs/concepts/how-liveblocks-works#Rooms)
in your application can store one document each, and these documents are
persisted on the cloud, visible on [your dashboard](/dashboard), and are
integrated into other Liveblocks products such as
[Comments](/docs/ready-made-features/comments) and
[Notifications](/docs/ready-made-features/notifications).

### Permanent storage

All documents created with Lexical are **permanently stored**. You don’t need to
sync, scale, or maintain any infrastructure.

### Users and mentions

Users and groups can be added to your document, and you can tag others inline.
You can also easily enable mention suggestions for both users and groups.

<Figure>
  <Image
    src="/assets/text-editor/mentions.png"
    alt="User mentions"
    width={1312}
    height={874}
  />
</Figure>

Users, groups, and mention suggestions can be added with
[`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers),
[`resolveGroupsInfo`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo),
and
[`resolveMentionSuggestions`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveMentionSuggestions).

```tsx highlight="6-16"
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      resolveUsers={async ({ userIds }) => {
        // Return user info from their `userIds`
        // ...
      }}
      resolveGroupsInfo={async ({ groupIds }) => {
        // Return group info from their `groupIds`
        // ...
      }}
      resolveMentionSuggestions={async ({ text, roomId }) => {
        // Return suggestions from the search `text`
        // Can return user IDs or mention objects for both users and groups
        // ...
      }}
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

### Realtime editing

Your document can be edited in real-time by multiple users at once. Each user
renders a cursor on screen that updates live as they move, select, and edit.

<Figure>
  <Image
    src="/assets/text-editor/cursors.png"
    alt="Real-time text cursors"
    width={1312}
    height={874}
  />
</Figure>

When [authenticating your users](/docs/authentication) with
[`prepareSession`](/docs/api-reference/liveblocks-node#access-tokens) or
[`identifyUser`](/docs/api-reference/liveblocks-node#id-tokens), pass `name` and
`color` properties to their `userInfo` to add their cursor information.

```js
userInfo: {
  name: "Marie",
  color: "#00ff00",
}
```

### Toolbars

Two toolbars are available allowing you to create a
[static toolbar](/docs/api-reference/liveblocks-react-lexical#Toolbar), or a
[floating toolbar](/docs/api-reference/liveblocks-react-lexical#FloatingToolbar),
as seen below.

<Figure>
  <Image
    src="/assets/text-editor/floating-toolbar.png"
    alt="FloatingToolbar"
    width={960}
    height={558}
  />
</Figure>

Add [`Toolbar`](/docs/api-reference/liveblocks-react-lexical#Toolbar) and
[`FloatingToolbar`](/docs/api-reference/liveblocks-react-lexical#FloatingToolbar)
to enable these features.

```tsx
<LexicalComposer initialConfig={initialConfig}>
  <LiveblocksPlugin>
    <Toolbar />
  </LiveblocksPlugin>
</LexicalComposer>
```

It’s easy to
[customize a toolbar’s content](/docs/api-reference/liveblocks-react-lexical#toolbar-extending-the-defaults),
piecing together sections like blocks.

```tsx
function CustomToolbar() {
  return (
    <Toolbar>
      // +++
      <Toolbar.BlockSelector />
      <Toolbar.SectionInline />
      <Toolbar.Separator />
      <Toolbar.SectionCollaboration />
      // +++
    </Toolbar>
  );
}
```

You can also easily create
[custom styled buttons](/docs/api-reference/liveblocks-react-lexical#Toolbar.Button),
and
[more](/docs/api-reference/liveblocks-react-lexical#creating-a-custom-toolbar),
with any functionality you like.

```tsx
function CustomButton() {
  return (
    // +++
    <Toolbar.Button
      name="Help"
      icon={<Icon.QuestionMark />}
      shortcut="CMD-H"
      onClick={() => console.log("help")}
    />
    // +++
  );
}
```

### Annotation comments

Add [Comments](/docs/ready-made-features/comments) to your text editor, allowing
others to select words in the editor, and leave annotations. Each annotation
creates a [thread](/docs/ready-made-features/comments/concepts#Threads), and
inside each you can reply, use emoji reactions, mention others, and more.

<Figure>
  <Image
    src="/assets/text-editor/annotations.png"
    alt="Text editor annotations"
    width={1312}
    height={874}
  />
</Figure>

Add a floating Comments composer to your text editor using
[`FloatingComposer`](/docs/api-reference/liveblocks-react-lexical#FloatingComposer).

```tsx highlight="3"
<LexicalComposer initialConfig={initialConfig}>
  <LiveblocksPlugin>
    <FloatingComposer />
  </LiveblocksPlugin>
</LexicalComposer>
```

Add [`useThreads`](/docs/ready-made-features/comments/hooks#threads-hook)
alongside
[`FloatingThreads`](/docs/api-reference/liveblocks-react-lexical#FloatingThreads)
and
[`AnchoredThreads`](/docs/api-reference/liveblocks-react-lexical#AnchoredThreads)
to render your comments to the page.

```tsx
export function Threads({ editor }) {
  const { threads } = useThreads();

  return (
    <>
      <AnchoredThreads threads={threads} />
      <FloatingThreads threads={threads} />
    </>
  );
}
```

`FloatingThreads` displays floating threads below text highlights in the editor,
ideal for mobile, whereas `AnchoredThreads` displays threads vertically
alongside the editor which is great on desktop. To create a new thread use the
button in a [pre-built toolbar](#Toolbars), or
[build a button yourself](/docs/api-reference/liveblocks-react-lexical#Opening-the-FloatingComposer).

### Notifications

Add a [Notifications](/docs/ready-made-features/notifications) UI to your
application, and automatically notify users when they’ve been mentioned.
Notifications also allows you to trigger sending emails, Slack, or any other
kind of notification, using our webhooks.

<Figure>
  <Image
    src="/assets/text-editor/notifications.png"
    alt="Text editor notifications"
    width={1312}
    height={874}
  />
</Figure>

Notifications is enabled by default, which means you just need to add our UI
components to keep your users notified. Follow our
[get started guides](/docs/get-started/notifications) to learn how to set up
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification).

```tsx
export function CollaborativeApp() {
  const { inboxNotifications } = useInboxNotifications();

  return (
    <InboxNotificationList>
      {inboxNotifications.map((inboxNotification) => (
        <InboxNotification
          key={inboxNotification.id}
          inboxNotification={inboxNotification}
        />
      ))}
    </InboxNotificationList>
  );
}
```

Inline mentions also trigger
[`TextMention`](/docs/platform/webhooks#TextMention-notification) notification
webhook events. Learn more about
[sending email notifications](/docs/ready-made-features/notifications/email-notifications).

### AI agents and editing on the server

Fetch and modify your text editor’s content from the server, enabling features
such as AI agents. Easily make changes that update in real-time for every
connected user.

<Figure>
  <Image
    src="/assets/text-editor/ai.png"
    alt="AI suggestions"
    width={1312}
    height={874}
  />
</Figure>

Use
[`withLexicalDocument`](/docs/api-reference/liveblocks-node-lexical#withLexicalDocument)
to get your editor’s content and make modifications live.

```tsx
await withLexicalDocument(
  { roomId: "my-room-id", client: liveblocks },
  async (doc) => {
    // Get editor content
    const textContent = doc.getTextContent();

    await doc.update(async () => {
      // Make real-time updates
      // ...
    });
  }
);
```

### Version History

Liveblocks Lexical enables version history for your document. Versions can be
automatically created when enabled in your project settings, or manually created
using the
[REST API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-version).
These versions can be easily displayed to users using a few simple components.
This allows for a comprehensive version history feature in your collaborative
text editor.

To implement version history in your application, you can use the
`useHistoryVersions` hook along with the `HistoryVersionsList` and
`HistoryVersionPreview` components. Here's an example of how to set this up:

```tsx
import {
  useHistoryVersions,
  HistoryVersionSummaryList,
} from "@liveblocks/react";
import { HistoryVersionPreview } from "@liveblocks/react-lexical";

function DocumentHistory() {
  const [selectedVersionId, setSelectedVersionId] = useState<string>();
  const { versions, isLoading } = useHistoryVersions();
  const selectedVersion = useMemo(
    () => versions?.find((version) => version.id === selectedVersionId),
    [selectedVersionId, versions]
  );

  if (isLoading) {
    return <div>Loading version history...</div>;
  }

  return (
    <>
      <div>
        {selectedVersion ? (
          <HistoryVersionPreview
            version={selectedVersion}
            className="h-full w-full"
            onVersionRestore={onVersionRestore}
          />
        ) : (
          <div>No version selected</div>
        )}
      </div>

      <div>
        <HistoryVersionSummaryList>
          {versions?.map((version) => (
            <HistoryVersionSummary
              onClick={() => {
                setSelectedVersionId(version.id);
              }}
              key={version.id}
              version={version}
              selected={version.id === selectedVersionId}
            />
          ))}
        </HistoryVersionSummaryList>
      </div>
    </>
  );
}
```

In this example, `useHistoryVersions` fetches the version history,
`HistoryVersionSummaryList` displays a list of available versions, and
`HistoryVersionPreview` shows a preview of the selected version. This setup
provides users with an interface to browse through the document's history and
restore different versions.

## Lexical API Reference

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="Lexical React"
    href="/docs/api-reference/liveblocks-react-lexical"
    description="@liveblocks/react-lexical"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Lexical Node"
    href="/docs/api-reference/liveblocks-node-lexical"
    description="@liveblocks/node-lexical"
    visual={<DocsApiIcon />}
  />
</ListGrid>

---
meta:
  title: "Tiptap"
  parentTitle: "Text editor"
  description: "Learn about using Multiplayer for Tiptap"
---

A quick overview of the packages for Tiptap.

## Concepts

Liveblocks Tiptap allows you to add collaboration to any
[Tiptap](https://tiptap.dev/) text editor, along with a number of related
features. Each [collaborative room](/docs/concepts/how-liveblocks-works#Rooms)
in your application can store one document each, and these documents are
persisted on the cloud, visible on [your dashboard](/dashboard), and are
integrated into other Liveblocks products such as
[Comments](/docs/ready-made-features/comments) and
[Notifications](/docs/ready-made-features/notifications).

### Permanent storage

All documents created with Tiptap are **permanently stored**. You don’t need to
sync, scale, or maintain any infrastructure.

### Users and mentions

Users and groups can be added to your document, and you can tag others inline.
You can also easily enable mention suggestions for both users and groups.

<Figure>
  <Image
    src="/assets/text-editor/mentions.png"
    alt="User mentions"
    width={1312}
    height={874}
  />
</Figure>

Users, groups, and mention suggestions can be added with
[`resolveUsers`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveUsers),
[`resolveGroupsInfo`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveGroupsInfo),
and
[`resolveMentionSuggestions`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveMentionSuggestions).

```tsx highlight="6-16"
import { LiveblocksProvider } from "@liveblocks/react/suspense";

function App() {
  return (
    <LiveblocksProvider
      resolveUsers={async ({ userIds }) => {
        // Return user info from their `userIds`
        // ...
      }}
      resolveGroupsInfo={async ({ groupIds }) => {
        // Return group info from their `groupIds`
        // ...
      }}
      resolveMentionSuggestions={async ({ text, roomId }) => {
        // Return suggestions from the search `text`
        // Can return user IDs or mention objects for both users and groups
        // ...
      }}
    >
      {/* children */}
    </LiveblocksProvider>
  );
}
```

### Realtime editing

Your document can be edited in real-time by multiple users at once. Each user
renders a cursor on screen that updates live as they move, select, and edit.

<Figure>
  <Image
    src="/assets/text-editor/cursors.png"
    alt="Real-time text cursors"
    width={1312}
    height={874}
  />
</Figure>

When [authenticating your users](/docs/authentication) with
[`prepareSession`](/docs/api-reference/liveblocks-node#access-tokens) or
[`identifyUser`](/docs/api-reference/liveblocks-node#id-tokens), pass `name` and
`color` properties to their `userInfo` to add their cursor information.

```js
userInfo: {
  name: "Marie",
  color: "#00ff00",
}
```

## Toolbars

Two toolbars are available allowing you to create a
[static toolbar](/docs/api-reference/liveblocks-react-tiptap#Toolbar), or a
[floating toolbar](/docs/api-reference/liveblocks-react-tiptap#FloatingToolbar),
as seen below.

<Figure>
  <Image
    src="/assets/text-editor/floating-toolbar.png"
    alt="FloatingToolbar"
    width={960}
    height={558}
  />
</Figure>

Add [`Toolbar`](/docs/api-reference/liveblocks-react-tiptap#Toolbar) and
[`FloatingToolbar`](/docs/api-reference/liveblocks-react-tiptap#FloatingToolbar)
to enable these features.

```tsx
<Toolbar editor={editor} />
```

It’s easy to
[customize a toolbar’s content](/docs/api-reference/liveblocks-react-tiptap#toolbar-extending-the-defaults),
piecing together sections like blocks.

```tsx
function CustomToolbar({ editor }: { editor: Editor | null }) {
  return (
    <Toolbar editor={editor}>
      // +++
      <Toolbar.BlockSelector />
      <Toolbar.SectionInline />
      <Toolbar.Separator />
      <Toolbar.SectionCollaboration />
      // +++
    </Toolbar>
  );
}
```

You can also easily create
[custom styled buttons](/docs/api-reference/liveblocks-react-tiptap#Toolbar.Button),
and
[more](/docs/api-reference/liveblocks-react-tiptap#creating-a-custom-toolbar),
with any functionality you like.

```tsx
function CustomButton() {
  return (
    // +++
    <Toolbar.Button
      name="Help"
      icon={<Icon.QuestionMark />}
      shortcut="CMD-H"
      onClick={() => console.log("help")}
    />
    // +++
  );
}
```

### AI features

We’re working on AI features for Tiptap, such as a floating AI toolbar. To get
involved, [request access to AI Copilots](/ai-copilots).

<Figure>
  <Image
    src="/assets/text-editor/ai.png"
    alt="AI suggestions"
    width={1312}
    height={874}
  />
</Figure>

### Annotations and comments

Add [Comments](/docs/ready-made-features/comments) to your text editor, allowing
others to select words in the editor, and leave annotations. Each annotation
creates a [thread](/docs/ready-made-features/comments/concepts#Threads), and
inside each you can reply, use emoji reactions, mention others, and more.

<Figure>
  <Image
    src="/assets/text-editor/annotations.png"
    alt="Text editor annotations"
    width={1312}
    height={874}
  />
</Figure>

Add a floating Comments composer to your text editor using
[`FloatingComposer`](/docs/api-reference/liveblocks-react-tiptap#FloatingComposer).

```tsx
<FloatingComposer editor={editor} />
```

Add [`useThreads`](/docs/ready-made-features/comments/hooks#threads-hook)
alongside
[`FloatingThreads`](/docs/api-reference/liveblocks-react-tiptap#FloatingThreads)
and
[`AnchoredThreads`](/docs/api-reference/liveblocks-react-tiptap#AnchoredThreads)
to render your comments to the page.

```tsx
export function Threads({ editor }) {
  const { threads } = useThreads();

  return (
    <>
      <AnchoredThreads editor={editor} threads={threads} />
      <FloatingThreads editor={editor} threads={threads} />
    </>
  );
}
```

`FloatingThreads` displays floating threads below text highlights in the editor,
ideal for mobile, whereas `AnchoredThreads` displays threads vertically
alongside the editor which is great on desktop. To create a new thread use the
button in a [pre-built toolbar](#Toolbars), or
[build a button yourself](/docs/api-reference/liveblocks-react-tiptap#Opening-the-composer).

### Notifications

Add a [Notifications](/docs/ready-made-features/notifications) UI to your
application, and automatically notify users when they’ve been mentioned.
Notifications also allows you to trigger sending emails, Slack, or any other
kind of notification, using our webhooks.

<Figure>
  <Image
    src="/assets/text-editor/notifications.png"
    alt="Text editor notifications"
    width={1312}
    height={874}
  />
</Figure>

Notifications is enabled by default, which means you just need to add our UI
components to keep your users notified. Follow our
[get started guides](/docs/get-started/notifications) to learn how to set up
[`InboxNotification`](/docs/api-reference/liveblocks-react-ui#InboxNotification).

```tsx
export function CollaborativeApp() {
  const { inboxNotifications } = useInboxNotifications();

  return (
    <InboxNotificationList>
      {inboxNotifications.map((inboxNotification) => (
        <InboxNotification
          key={inboxNotification.id}
          inboxNotification={inboxNotification}
        />
      ))}
    </InboxNotificationList>
  );
}
```

Inline mentions also trigger
[`TextMention`](/docs/platform/webhooks#TextMention-notification) notification
webhook events. Learn more about
[sending email notifications](/docs/ready-made-features/notifications/email-notifications).

## Server-side modifications

Fetch and modify your text editor’s content from the server, enabling features
such as AI agents. Easily make changes that update in real-time for every
connected user. Use
[`withProsemirrorDocument`](/docs/api-reference/liveblocks-node-prosemirror#withProsemirrorDocument)
to get your editor’s content and make modifications live.

```tsx
await withProsemirrorDocument(
  { roomId: "your-room-id", client: liveblocks },
  async (api) => {
    // get editor content
    const text = api.getText();

    await api.update((doc, tr) => {
      // Make real-time updates
      // ...
    });
  }
);
```

### Multiple editors

It’s possible to use multiple Tiptap editors on a single page, and each editor
having its own collaboration and extensions. You can enable this by giving each
editor a unique `field` value.

```tsx
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";

function TextEditor() {
  const liveblocks = useLiveblocksExtension({
    field: "editor-one",
  });

  // ...
}
```

Learn more about
[using multiple editors](/docs/api-reference/liveblocks-react-tiptap#Multiple-editors).

### Offline support

Liveblocks Tiptap has an experimental option that enables offline support.
Offline support means that once a document has been opened, it’s saved locally
on the browser, and can be shown instantly without a loading screen. Enable this
by passing a `offlineSupport_experimental` value.

```tsx
import { useLiveblocksExtension } from "@liveblocks/react-tiptap";

function TextEditor() {
  const liveblocks = useLiveblocksExtension({
    offlineSupport_experimental: true,
  });

  // ...
}
```

Learn more about
[offline support](/docs/api-reference/liveblocks-react-tiptap#Offline-support).

### Version History

Liveblocks Tiptap enables version history for your document. Versions can be
automatically created when enabled in your project settings, or manually created
using the
[REST API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-version).
These versions can be easily displayed to users using a few simple components.
This allows for a comprehensive version history feature in your collaborative
text editor.

To implement version history in your application, you can use the
`useHistoryVersions` hook along with the `HistoryVersionsList` and
`HistoryVersionPreview` components. Here's an example of how to set this up:

```tsx
import {
  useHistoryVersions,
  HistoryVersionSummaryList,
} from "@liveblocks/react";
import { HistoryVersionPreview } from "@liveblocks/react-tiptap";

function DocumentHistory() {
  const [selectedVersionId, setSelectedVersionId] = useState<string>();
  const { versions, isLoading } = useHistoryVersions();
  const selectedVersion = useMemo(
    () => versions?.find((version) => version.id === selectedVersionId),
    [selectedVersionId, versions]
  );

  if (isLoading) {
    return <div>Loading version history...</div>;
  }

  return (
    <>
      <div>
        {selectedVersion ? (
          <HistoryVersionPreview
            version={selectedVersion}
            className="h-full w-full"
            onVersionRestore={onVersionRestore}
          />
        ) : (
          <div>No version selected</div>
        )}
      </div>

      <div>
        <HistoryVersionSummaryList>
          {versions?.map((version) => (
            <HistoryVersionSummary
              onClick={() => {
                setSelectedVersionId(version.id);
              }}
              key={version.id}
              version={version}
              selected={version.id === selectedVersionId}
            />
          ))}
        </HistoryVersionSummaryList>
      </div>
    </>
  );
}
```

In this example, `useHistoryVersions` fetches the version history,
`HistoryVersionSummaryList` displays a list of available versions, and
`HistoryVersionPreview` shows a preview of the selected version. This setup
provides users with an interface to browse through the document's history and
restore different versions.

## Tiptap API Reference

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="Tiptap React"
    href="/docs/api-reference/liveblocks-react-tiptap"
    description="@liveblocks/react-tiptap"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="Tiptap/ProseMirror Node"
    href="/docs/api-reference/liveblocks-node-prosemirror"
    description="@liveblocks/node-prosemirror"
    visual={<DocsApiIcon />}
  />
</ListGrid>

---
meta:
  title: "Notifications"
  parentTitle: "Ready-made features"
  description:
    "Embed a customizable notification experience into your product to enable
    people to collaborate."
---

Embed a notification experience into your product, enabling collaboration
between your users. Liveblocks Notifications is fully customizable, allowing you
to create a range of notification experiences that look and feel like your
product. Easily send notifications, create inbox notification trays, and trigger
emails with aggregated notifications. Any custom notification can be sent, but
other parts of Liveblocks, such as
[Comments](/docs/ready-made-features/comments) and
[text editors](/docs/ready-made-features/multiplayer-editing/text-editor), can
also automatically trigger collaboration notifications where appropriate.

## Overview

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="Concepts"
    href="/docs/ready-made-features/notifications/concepts"
    description="Learn about inbox notifications"
    visual={<DocsIdeaIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Default components"
    href="/docs/ready-made-features/notifications/default-components"
    description="Ready-to-use customizable components"
    visual={
      <DocsDefaultComponentsIcon className="fill-product-icon-brand h-auto w-6" />
    }
  />
  <DocsCard
    type="technology"
    title="Hooks"
    href="/docs/ready-made-features/notifications/hooks"
    description="Retrieve notifications & set settings"
    visual={<DocsHooksIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Styling and customization"
    href="/docs/ready-made-features/notifications/styling-and-customization"
    description="CSS variables, dark mode, localization"
    visual={<DocsStylingIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Email notifications"
    href="/docs/ready-made-features/notifications/email-notifications"
    description="Send notifications with webhooks"
    visual={<DocsEmailIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Notifications API Reference

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="React"
    href="/docs/api-reference/liveblocks-react#Notifications"
    description="@liveblocks/react"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="React UI"
    href="/docs/api-reference/liveblocks-react-ui#Notifications"
    description="@liveblocks/react-ui"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="JavaScript"
    href="/docs/api-reference/liveblocks-client#Notifications"
    description="@liveblocks/client"
    visual={<DocsJavascriptIcon />}
  />
  <DocsCard
    type="technology"
    title="Node.js"
    href="/docs/api-reference/liveblocks-node#Notifications"
    description="@liveblocks/node"
    visual={<DocsNodejsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="REST API"
    href="/docs/api-reference/rest-api-endpoints#Notifications"
    description="HTTP endpoints"
    visual={<DocsApiIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Examples using Notifications

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments notifications",
      slug: "comments-notifications",
      image: "/images/examples/thumbnails/comments-notifications.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Custom notifications",
      slug: "notifications-custom",
      image: "/images/examples/thumbnails/custom-notifications.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Comments emails",
      slug: "comments-emails",
      image: "/images/examples/thumbnails/comments-email.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Text editor emails",
      slug: "collaborative-text-editor-emails",
      image: "/images/examples/thumbnails/text-editor-email.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Concepts"
  parentTitle: "Notifications"
  description: "Learn about Notifications"
---

A quick overview of the concepts used in Liveblocks Notifications.

## Inbox Notifications

In Liveblocks Notifications, everything revolves around _inbox notifications_.
Each user can receive these notifications, and a list of them can be rendered in
your application to create a notification tray. Inbox notifications can be sent
either automatically by other Liveblocks products, or manually by calling a
function on the server.

<Figure>
  <Image
    src="/assets/notifications/custom-notification.jpg"
    alt="Thread"
    width={1344}
    height={896}
  />
</Figure>

A user’s inbox notifications can be retrieved by a
[React hook](/docs/api-reference/liveblocks-react#useInboxNotifications) or
[individually on your server](/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications-inboxNotificationId),
which is helpful after receiving a
[notification webhook event](/docs/platform/webhooks#NotificationEvent). Here’s
an example of an inbox notification object for a custom notification.

```ts
{
  id: "in_sfj29g...",
  kind: "$myCustomNotification",
  subjectId: "myCustomId_dxs9s0",
  readAt: Date <Fri Dec 16 2023 15:16:25 GMT+0000 (Greenwich Mean Time)>,
  notifiedAt: Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>,
  activities: [
    { myCustomProperty: "myCustomValue" },
  ]
}
```

To build a notification tray, you can use a combination of
[default components](/docs/ready-made-features/notifications/default-components)
and [hooks](/docs/ready-made-features/notifications/hooks).

## Project-based

Inbox notifications are
[project-based](/docs/concepts/how-liveblocks-works#Projects), which means that
a user can receive notifications that were sent from other
[rooms](/docs/concepts/how-liveblocks-works#Rooms). This differs to other
Liveblocks products which are room-based, and rely on users inhabiting the same
space.

<Figure>
  <Image
    src="/assets/notifications/notification-rooms.jpg"
    alt="Notification rooms"
    width={1344}
    height={896}
  />
</Figure>

Project-based components require only
[`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider)
in React, which is different to other room-based Liveblocks products which also
require [`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider).

## Notification kinds

Each inbox notification has a `kind`, and this represents the type of
notification it is. A `kind` is either a custom string prefixed with `$` (e.g.
`$myCustomNotification`) when sending a custom notification, or a predefined
string set by Liveblocks (e.g. `thread`) when it’s sent by a Liveblocks product.

### Custom notifications

A custom notification is a notification that you specify yourself, and can
contain any content.

<Figure>
  <Image
    src="/assets/notifications/custom-notification-both.jpg"
    alt="Thread"
    width={1344}
    height={896}
  />
</Figure>

Custom notifications can be sent using
[`liveblocks.triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger)
on the server, and the `kind` must always begin with a `$`, for example
`$myCustomNotification`.

When you trigger an inbox notification, you pass the activity data, which
represents the events that happened. If you’d like to update an existing inbox
notification when a new activity happens, you can
[configure the custom notification to use batching](#notification-batching).

### Collaboration notifications

[Comments](/docs/ready-made-features/comments) can send `thread` notifications
for you automatically, notifying you of events such as a mention, or a new reply
in a thread you’re participating in. Additionally,
[text editors](/docs/ready-made-features/multiplayer-editing) can send
`textMention` notifications when users are mentioned inside the editor.

<Figure>
  <Image
    src="/assets/notifications/notifications-inbox.jpg"
    alt="Thread"
    width={1344}
    height={896}
  />
</Figure>

These are enabled by default and the notification `kind` names are `thread` and
`textMention`. Threads and mentions leverage Notifications behind-the-scenes to
keep track of unread comments so viewing a thread will also mark its inbox
notification as read, and vice versa.

If you use the default
[`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component, it will
automatically handle marking threads as read when they are viewed and show
unread indicators when there are unread comments in threads you are
participating in.

<Figure>
  <Image
    src="/assets/comments/thread-unread-indicator.png"
    alt="Unread indicator in Thread component"
    width={1456}
    height={896}
  />
</Figure>

## Notification batching

Batching allows you to group multiple activities into a single notification.
This is useful for reducing the number of notifications that are sent to the
user, and can be enabled on custom notification kinds. It’s always enabled for
Liveblocks `thread` notifications, where new comments in a thread are grouped
into a single notification.

Within the “Notifications” page in the [dashboard](/dashboard), you can add a
custom kind and enable batching. 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.

<Figure>
  <video width={1512} height={982} autoPlay loop muted playsInline>
    <source
      src="/assets/notifications/notification-batching.mp4"
      type="video/mp4"
    />
  </video>
</Figure>

An inbox notification can have up to 50 activities, if you exceed this number, a
new inbox notification will be created. Liveblocks `textMention` notifications
are never batched.

## Notification channels

You can also send notifications on other channels, such as email or Slack,
letting users know when they’ve missed in-app notifications. Using our dashboard
and hooks you can allow users to set their own notification settings.

<Figure>
  <Image
    src="/assets/notifications/notification-settings.jpg"
    alt="Notification settings"
    width={1344}
    height={896}
  />
</Figure>

There are default channel types for email, Slack, Microsoft Teams, and Web Push.
Each channel works the same, but you can enable different settings for each and
use them differently. Learn more about notification channels in our
[email notifications](/docs/ready-made-features/notifications/email-notifications)
page.

## Data flow

An unread inbox notification will trigger a notification based on the project
notification settings and the user notification settings. Liveblocks evaluates
both the project-level configuration and user preferences to determine whether
to send a channel notification.

<Figure>
  <Image
    src="/assets/notifications/notification-flow.png"
    alt="Notification flow"
    width={672}
    height={990}
  />
</Figure>

## Permissions

### Access tokens

When using [access token authentication](/docs/authentication/access-token),
Liveblocks disregards the permissions specified in the tokens and returns all
notifications created for the user. Consequently, a user will receive
notifications for mentions in comments within rooms they do not have access to.

For greater control over notification permissions, we recommend utilizing
[ID tokens](/docs/authentication/id-token).

### ID token

When using [ID token authentication](/docs/authentication/id-token), Liveblocks
reads the permissions set on the room, and uses these to deliver notifications
that the user is authorized to view.

## Email notifications

It’s possible to send email notifications to your users, listing all their
unread inbox notifications using a [webhook](/docs/platform/webhooks) event that
Liveblocks provides. Learn more about
[email notifications](/docs/ready-made-features/notifications/email-notifications).

---
meta:
  title: "Default components"
  parentTitle: "Notifications"
  description: "Ready-to-use customizable components"
---

The default components included in Notifications are a great way to start
building your application. With these components you can render inbox
notification components.

- Fully styled notification components, with an optional dark mode.
- Customize through CSS variables and class names.
- Receive collaboration notifications automatically triggered by Liveblocks
  [Comments](/docs/ready-made-features/comments) or
  [text editors](/docs/ready-made-features/multiplayer-editing/text-editor).
- Receive fully custom notifications.

## InboxNotification

The [`InboxNotification`][] component renders a single inbox notification.

<Figure>
  <Image
    src="/assets/notifications/inbox-notification.png"
    alt="InboxNotification"
    width={768}
    height={446}
  />
</Figure>

### Usage

The best way to get started is to import the [`useInboxNotifications`][] hook,
and loop through each of the current user’s notifications.

```tsx highlight="5,10-13"
import { InboxNotification } from "@liveblocks/react-ui";
import { useInboxNotifications } from "../liveblocks.config";

function Component() {
  const { inboxNotifications } = useInboxNotifications();

  return (
    <>
      {inboxNotifications.map((inboxNotification) => (
        <InboxNotification
          key={inboxNotification.id}
          inboxNotification={inboxNotification}
        />
      ))}
    </>
  );
}
```

#### Room titles

Room titles displayed in notifications are obtained using the
[`resolveRoomsInfo`](/docs/api-reference/liveblocks-react#LiveblocksProviderResolveRoomsInfo)
function in
[`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider).
This allows you to display meaningful document or room names instead of room IDs
in your notifications. Learn more in our guide on
[how to customize room names in inbox notifications](/guides/how-to-customize-room-names-in-inbox-notifications).

#### Rendering different components

As well as displaying the default notification component, you can render
alternate components for different notification `kinds` (the type of
notification). Below we’re rendering a different component for custom
`$fileUploaded` notifications.

```tsx highlight="14-18"
import { InboxNotification } from "@liveblocks/react-ui";
import { useInboxNotifications } from "../liveblocks.config";

function Component() {
  const { inboxNotifications } = useInboxNotifications();

  return (
    <>
      {inboxNotifications.map((inboxNotification) => (
        <InboxNotification
          key={inboxNotification.id}
          inboxNotification={inboxNotification}
          kinds={{
            $fileUploaded: (props) => (
              <InboxNotification.Custom {...props} title="New file" aside="📁">
                A new file has been uploaded
              </InboxNotification.Custom>
            ),
          }}
        />
      ))}
    </>
  );
}
```

You can also render any plain JSX you like. Learn more about this under
[rendering notification kinds differently](/docs/api-reference/liveblocks-react-ui#Rendering-notification-kinds-differently).

## InboxNotificationList

The [`InboxNotificationList`][] component renders your inbox notifications as a
list.

<Figure>
  <Image
    src="/assets/notifications/inbox-notification-list.png"
    alt="InboxNotificationList"
    width={768}
    height={446}
  />
</Figure>

### Usage

Wrap your [`InboxNotification`][] components in [`InboxNotificationList`][] to
render your notifications as an ordered HTML list, `ol > li`.

```tsx highlight="8,15"
import { InboxNotification, InboxNotificationList } from "@liveblocks/react-ui";
import { useInboxNotifications } from "../liveblocks.config";

function Component() {
  const { inboxNotifications } = useInboxNotifications();

  return (
    <InboxNotificationList>
      {inboxNotifications.map((inboxNotification) => (
        <InboxNotification
          key={inboxNotification.id}
          inboxNotification={inboxNotification}
        />
      ))}
    </InboxNotificationList>
  );
}
```

[`InboxNotification`]: /docs/api-reference/liveblocks-react-ui#InboxNotification
[`InboxNotificationList`]:
  /docs/api-reference/liveblocks-react-ui#InboxNotificationList
[`useInboxNotifications`]:
  /docs/api-reference/liveblocks-react#useInboxNotifications

## Customization

It’s possible to style and localize the default components:

- Import dark mode styles.
- Modify the style with CSS variables and class names.
- Use overrides to change default text used in the components.

Learn more under
[styling and customization](/docs/ready-made-features/notifications/styling-and-customization).

---
meta:
  title: "Email notifications"
  parentTitle: "Notifications"
  description: "Send notifications with webhooks"
---

Using Liveblocks webhooks, it’s possible to trigger your API endpoints when
certain events occur, such as users joining or leaving rooms. One of these
events was created specifically for sending notifications from your back end,
the "`notification"` event. Certain notifications can be enabled or disabled on
different channels for this event, for example you could enable notifications on
`email` and `slack`, but not on `teams`.

<Figure>
  <Image
    src="/assets/comments/email-notification.png"
    alt="An email titled 'New notifications' showing two comments and a link to the thread"
    width={1500}
    height={1000}
  />
</Figure>

## Step-by-step guides

This page is an overview—if you’re looking to send email notifications after
Comments or Text Editor updates, makes sure to read our specific guides:

- [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).

## Inbox notifications

Email notifications are built around the concept of inbox notifications, which
are different from “normal” notifications in the sense that they can group
multiple activities together and evolve over time. This makes more sense when
sending email notifications because it helps to avoid flooding your users with
too many emails.

## Sending email notifications with webhooks

Using [Liveblocks webhooks](/docs/platform/webhooks#Liveblocks-events) you can
listen to a range of events such as user joining rooms, or threads being
created. On [your dashboard](https://liveblocks.io/dashboard) you can create a
webhook for a project, and select which events you’d like to listen to.

<Figure>
  <video width={1512} height={982} autoPlay loop muted playsInline>
    <source src="/assets/webhooks/edit-events.mp4" type="video/mp4" />
  </video>
</Figure>

The endpoint URL you pass will receive request with relevant data when the event
occurs. The webhook event built for creating these unread notification emails is
called `"notification"`, and by default is sent up to every 30 minutes to each
user, though this can be customized in the webhooks dashboard.

### Notification channels

`"notification"` webhooks can be enabled and disabled on certain channels in the
notifications dashboard page. Channels are used to represent different places
your users may receive notifications, such as on `email`, `slack`, `teams`, and
`webPush`.

<Figure>
  <video width={1512} height={982} autoPlay loop muted playsInline>
    <source src="/assets/webhooks/notification-settings.mp4" type="video/mp4" />
  </video>
</Figure>

### Event object

Here’s an example of an event object that’s sent when a user receives a new (or
updated) inbox notification. Note that this is custom notification sent on the
`email` channel.

```ts
const event = {
  type: "notification",
  data: {
    channel: "email",
    kind: "$myCustomNotification",
    projectId: "my-project-id",
    roomId: "my-room-id",
    userId: "my-user-id",
    inboxNotificationId: "in_xt3p7ak...",
    createdAt: "2021-10-06T01:45:56.558Z",
  },
};
```

### Your endpoint

In your endpoint, you can use this `event` object with
[`liveblocks.getInboxNotification`](/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications-inboxNotificationId),
which will return the inbox notification for the event.

```ts
// Data from the `notification` event
const { inboxNotificationId, userId } = event.data;

// Get the inbox notification, which details when the user last read the thread
const inboxNotification = await liveblocks.getInboxNotification({
  inboxNotificationId,
  userId,
});

// { kind: "$myCustomNotification", readAt: Date<2024-07-13T14:32:50.697Z>, ... }
console.log(inboxNotification);
```

Once you have the inbox notification you can then send an email to the `userId`
containing information from the notification.

```ts
const emailAddress = __getUserEmail__(userId);

// Send email to the user that received the inbox notification
__sendEmail__({
  from: "hello@my-company.com",
  to: emailAddress,
  title: "New notification",
  html: `
    <h1>New notification</h1>
    <a href="...">Learn more</a>
  `,
});
```

Here’s an example with every step linked together, along with the code necessary
to verify a webhook request is valid.

```ts title="Full example" isCollapsable isCollapsed
import {
  Liveblocks,
  WebhookHandler,
  stringifyCommentBody,
} from "@liveblocks/node";

// Add your webhook secret key from a project's webhooks dashboard
const WEBHOOK_SECRET = "YOUR_WEBHOOK_SECRET";
const webhookHandler = new WebhookHandler(WEBHOOK_SECRET);

// Add your secret key from a project's API keys dashboard
const API_SECRET = "{{SECRET_KEY}}";
const liveblocks = new Liveblocks({ secret: API_SECRET });

export async function POST(request: Request) {
  const body = await request.json();
  const headers = request.headers;

  // Verify if this is a real webhook request
  let event;
  try {
    event = webhookHandler.verifyRequest({
      headers: headers,
      rawBody: JSON.stringify(body),
    });
  } catch (err) {
    console.error(err);
    return new Response("Could not verify webhook call", { status: 400 });
  }

  // When a notification webhook event has triggered
  if (event.type === "notification") {
    const { inboxNotificationId, userId } = event.data;

    // Get the inbox notification, which details when the user last read the thread
    const inboxNotification = await liveblocks.getInboxNotification({
      inboxNotificationId,
      userId,
    });

    // Send email to the user that received the inbox notification
    const emailAddress = __getUserEmail__(userId);
    __sendEmail__({
      from: "hello@my-company.com",
      to: emailAddress,
      title: "New comment",
      html: `
        <h1>New notification</h1>
        <a href="...">Learn more</a>
      `,
    });
  }

  return new Response(null, { status: 200 });
}
```

<Banner title="Sending Comments emails">

This page is an overview of creating email notifications with Notifications. If
you’d like to send emails about new comments, make sure to read
[how to send email notifications of unread comments](/docs/guides/how-to-send-email-notifications-of-unread-comments).

</Banner>

### Permissions

When you receive a `notification` webhook event, it's essential to verify if the
user has access to the room before sending an email. Liveblocks lacks the
necessary information to determine if a user has access to a room. For instance,
we create an inbox notification when a user is mentioned in a comment. In this
user's client context, we can determine if they have access to the notification
thanks to the token generated for that user. However, when we send a
`notification` webhook event, we lack this information.

#### Access token authentication

If you are using access tokens, this will always be true, Liveblocks will never
have the information.

#### ID token authentication

If you are using ID tokens, Liveblocks already possesses certain information
about the permissions you have configured for each room, specifying which users
and groups have access. However, what we currently lack is the relationship
between a user and a group. At present, you need to verify user access before
sending an email. We do, however, plan to include full permissions info in
Liveblocks in our future updates. If you're interested in learning more about
this feature, please feel free to reach out to us.

## Notification settings

It’s possible to allow users to choose their own notification settings for each
channel in your application.

<Figure>
  <Image
    src="/assets/notifications/notification-settings.jpg"
    alt="Notification settings"
    width={1344}
    height={896}
  />
</Figure>

To implement this, import
[`useNotificationSettings`](/docs/api-reference/liveblocks-react#useNotificationSettings)
into your component, and create a list of toggles. Here’s an example of a
checkbox that toggles thread notification emails.

```tsx highlight="4,8-17"
import { useNotificationSettings } from "@liveblocks/react/suspense";

function NotificationSettings() {
  const [{ settings }, updateSettings] = useNotificationSettings();

  return (
    <>
      <label>
        Receive thread notifications by email:
        <input
          type="checkbox"
          checked={settings.email?.thread}
          onChange={(e) =>
            updateSettings({ email: { thread: e.target.checked } })
          }
        />
      </label>
      {/* Other checkboxes */}
    </>
  );
}
```

## Retrieving and modifying Comments data

Here’s every Notifications-related
[`@liveblocks/node`](/docs/api-reference/liveblocks-node) function. Each also
has a corresponding REST API, you can find more info by following the links.

- [`getInboxNotification`](/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications-inboxNotificationId)
- [`triggerInboxNotification`](/docs/api-reference/liveblocks-node#post-inbox-notifications-trigger)

---
meta:
  title: "Hooks"
  parentTitle: "Notifications"
  description: "Get inbox notifications"
---

The Notifications React hooks can be used to fetch inbox notifications on the
client, and change their behavior.

## List a user’s inbox notifications [#inbox-notifications-hook]

The most important Notifications hook is [`useInboxNotifications`][], which
retrieves every inbox notification for the current user. This can be used to
render a list of notifications, either using the
[default components](/docs/ready-made-features/comments/default-components), or
with your own components. Here’s an example of it used with the default
[`InboxNotification`][] component.

```tsx
import { useInboxNotifications } from "../liveblocks.config";

function Component() {
  const { inboxNotifications } = useInboxNotifications();

  // [{ kind: "$myCustomNotification", id: "in_sf8s6sh...", ... }, ...]
  console.log(inboxNotifications);

  return (
    <>
      {inboxNotifications.map((inboxNotification) => (
        <InboxNotification
          key={inboxNotification.id}
          inboxNotification={inboxNotification}
        />
      ))}
    </>
  );
}
```

There are two versions of the [`useInboxNotifications`][] hook, the
[Suspense version](#suspense-hooks), which we recommend by default, and the
[regular version](#regular-hooks).

## Get unread notifications count [#unread-notifications-count-hook]

The
[`useUnreadInboxNotificationsCount`](/docs/api-reference/liveblocks-react#useUnreadInboxNotificationsCount)
hook is useful for creating a badge that displays the unread notifications
count.

```tsx
import { useUnreadInboxNotificationsCount } from "../liveblocks.config";

function Component() {
  const { count } = useUnreadInboxNotificationsCount();

  // 3
  console.log(count);

  return <div>You have {count} unread notifications</div>;
}
```

## Mark all notifications as read

To mark all notifications as read for the current user, you can add
[`useMarkAllInboxNotificationsAsRead`](/docs/api-reference/liveblocks-react#useMarkAllInboxNotificationsAsRead).

```tsx
function NotificationReadButton() {
  const markAllInboxNotificationsAsRead = useMarkAllInboxNotificationsAsRead();

  return (
    <button onClick={markAllInboxNotificationsAsRead}>Mark all as read</button>
  );
}
```

## Fetch a user’s information [#user-hook]

The only information Liveblocks stores about users is their user ID, which is
set when [authenticating with Liveblocks](/docs/authentication). With the
[`useUser`](/docs/api-reference/liveblocks-react#useUser) hook, you can fetch a
user’s information from their ID. This is particularly helpful when building
custom components, as this allows you fetch their name, avatar, and any other
custom data you’ve set.

```tsx
import { useUser } from "../liveblocks.config";

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

  // { name: "Olivier", avatar: "https://...", color: "red" }
  console.log(user);

  return <img src={user.avatar} alt={user.name} />;
}
```

The user data retrieved is set within the
[`resolveUsers`](/docs/api-reference/liveblocks-client#resolveUsers) function in
your `liveblocks.config.ts` file.

```ts
async function resolveUsers({ userIds }) {
  // ["olivier@example.com"]
  console.log(userIds);

  return [
    {
      name: "Olivier",
      avatar: "https://example.com/olivier.png",
      color: "red",
    },
  ];
}
```

There are two versions of the
[`useUser`](/docs/api-reference/liveblocks-client#useUser) hook,
[Suspense](#suspense-hooks), which we recommend by default, and
[regular](#regular-hooks).

## Fetch a group’s information [#group-hook]

Similar to users, Liveblocks only stores group IDs for groups. With the
[`useGroupInfo`](/docs/api-reference/liveblocks-react#useGroupInfo) hook, you
can fetch a group’s information from their ID. This is particularly helpful when
building custom components, as this allows you to fetch their name, avatar, and
any other custom data you’ve set.

```tsx
import { useGroupInfo } from "../liveblocks.config";

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

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

  return <img src={group.avatar} alt={group.name} />;
}
```

The group data retrieved is set within the
[`resolveGroupsInfo`](/docs/api-reference/liveblocks-client#resolveGroupsInfo)
function in your `liveblocks.config.ts` file.

```ts
async function resolveGroupsInfo({ groupIds }) {
  // ["engineering"]
  console.log(groupIds);

  return [
    {
      name: "Engineering",
      avatar: "https://example.com/engineering.png",
      color: "blue",
    },
  ];
}
```

There are two versions of the
[`useGroupInfo`](/docs/api-reference/liveblocks-client#useGroupInfo) hook,
[Suspense](#suspense-hooks), which we recommend by default, and
[regular](#regular-hooks).

## Comments hooks

[Comments](/docs/ready-made-features/comments) integrates seamlessly into
Notifications, and a number of hooks are provided to modify this behavior.

### Unread indicator

Threads keep track of unread comments so viewing a thread will also mark its
inbox notification as read, and vice versa. If you use the default
[`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component, it will
automatically handle marking threads as read when they are viewed and show
unread indicators when there are unread comments in threads you are
participating in.

<Figure>
  <Image
    src="/assets/comments/thread-unread-indicator.png"
    alt="Unread indicator in Thread component"
    width={1456}
    height={896}
  />
</Figure>

If you’re building your own custom `Thread` component, you can use
[`useMarkThreadAsRead`](/docs/api-reference/liveblocks-react#useMarkThreadAsRead)
and
[`useThreadSubscription`](/docs/api-reference/liveblocks-react#useThreadSubscription)
to replicate or customize this behavior.

### Thread subscription settings

By default, you’ll receive inbox notifications for threads you are participating
in. This setting can be customized for each user and per room:

- `"all"` to be notified about everything.
- `"replies_and_mentions"` for the default setting.
- `"none"` to mute the room.

You can use
[`useRoomSubscriptionSettings`](/docs/api-reference/liveblocks-react#useRoomSubscriptionSettings)
to build a settings picker in your app and allow users to change their own
subscription settings for the current room, or use
[`updateRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-subscription-settings)
server-side to control them yourself: for example, to automatically make a
document’s author notified about everything in their document.

### Getting thread data

When using inbox notifications for threads, it can be useful to fetch the actual
thread data, and
[`useInboxNotificationThread`](/docs/api-reference/liveblocks-react#useInboxNotificationThread)
makes this easy.

```tsx
function Notification({ inboxNotification }) {
  const thread = useInboxNotificationThread(inboxNotification.id);

  // { type: "thread", id: "th_sf8s6sh...", ... }
  console.log(thread);

  // ...
}
```

## Hook types [#hook-types]

There are two different ways to use the
[notifications](#inbox-notifications-hook) and [user](#user-hook) hooks; with
[React Suspense](https://react.dev/reference/react/Suspense), and without it. We
recommend using the Suspense versions, as they often result in simpler code.

### Suspense hooks [#suspense-hooks]

Using Suspense hooks means that any data retrieved, for example
`inboxNotifications` from `useInboxNotifications`, will never be `undefined`,
and your component will never see an error.

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

// Suspense: `inboxNotifications` is always defined
function MyNotifications() {
  const { inboxNotifications } = useInboxNotifications();

  // [{ type: "$myCustomNotification", id: "in_sf8s6sh...", ... }, ...]
  console.log(inboxNotifications);
}
```

To catch errors and display a loading screen, you can use
[`ErrorBoundary`](https://www.npmjs.com/package/react-error-boundary) and
[`ClientSideSuspense`](/docs/api-reference/liveblocks-react#suspense-avoid-ssr).

```tsx highlight="7-11"
import { ClientSideSuspense } from "@liveblocks/react/suspense";
import { ErrorBoundary } from "react-error-boundary";

// Handle errors and loading state in the component above
function Component() {
  return (
    <ErrorBoundary fallback={<div>Error</div>}>
      <ClientSideSuspense fallback={<div>Loading...</div>}>
        <MyNotifications />
      </ClientSideSuspense>
    </ErrorBoundary>
  );
}
```

To use Suspense, make sure you’re exporting your hooks from
`@liveblocks/react/suspense`.

```tsx
// Suspense version of hooks
import { useInboxNotifications, useUser } from "@liveblocks/react/suspense";
//                                                                ^^^^^^^^
```

### Regular hooks [#regular-hooks]

The regular versions of Liveblocks hooks require you to check for `error` and
`isLoading` properties. You can then handle these states in the same component.

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

// Handle errors and loading state in the same component
function MyNotifications() {
  const { inboxNotifications, error, isLoading } = useInboxNotifications();

  if (error) {
    return <div>Error</div>;
  }

  if (isLoading) {
    return <div>Loading...</div>;
  }

  // Non-Suspense: `inboxNotifications` is only defined AFTER the `if` checks
  // [{ type: "$myCustomNotification", id: "in_sf8s6sh...", ... }, ...]
  console.log(inboxNotifications);
}
```

To use the regular hooks, make sure you’re exporting from `@liveblocks/react`.

```tsx
// Regular version of hooks
import { useInboxNotifications, useUser } from "@liveblocks/react";
//                                              ^^^^^^^^^^^^^^^^^
```

[`InboxNotification`]: /docs/api-reference/liveblocks-react-ui#InboxNotification
[`InboxNotificationList`]:
  /docs/api-reference/liveblocks-react-ui#InboxNotificationList
[`useInboxNotifications`]:
  /docs/api-reference/liveblocks-react#useInboxNotifications

---
meta:
  title: "Styling and customization"
  parentTitle: "Notifications"
  description: "CSS variables, dark mode, and more"
---

Styling Notifications
[default components](/docs/ready-made-features/notifications/default-components)
is enabled through a range of means, such as CSS variables, class names, and
more.

## Default components

To add the default components’ theme, import the
[default styles](/docs/api-reference/liveblocks-react-ui#Default-styles) CSS
file.

```ts
import "@liveblocks/react-ui/styles.css";
```

You can also import one of two CSS files to enable
[dark mode](/docs/api-reference/liveblocks-react-ui#Dark-mode), depending on how
you’d like to enable it.

```ts
// Dark mode using the system theme with `prefers-color-scheme`
import "@liveblocks/react-ui/styles/dark/media-query.css";
```

```ts
// Dark mode using `className="dark"`, `data-theme="dark"`, or `data-dark="true"`
import "@liveblocks/react-ui/styles/dark/attributes.css";
```

### CSS variables

A number of
[CSS variables](/docs/api-reference/liveblocks-react-ui#CSS-variables) can be
used to customize colors, spacing, and more. This is our recommended path for
styling the default components, as you can quickly and easily modify all
components with just a few variables.

```css
/* Styles all default components */
.lb-root {
  --lb-accent: purple;
  --lb-spacing: 1em;
  --lb-radius: 0;
}
```

### Class names

Should you need deeper customization,
[class names](/docs/api-reference/liveblocks-react-ui#Class-names) can be
styled, some of which provide contextual data attributes.

```css
.lb-notification {
  /* Customize notification */
}

.lb-avatar[data-loading] {
  /* Customize avatar loading state */
}
```

---
meta:
  title: "DevTools"
  parentTitle: "Tools"
  description:
    "Learn how to leverage Liveblocks DevTools to inspect and troubleshoot your
    collaborative application."
---

Our [DevTools](/devtools) is a browser-based extension that integrates with
Liveblocks and your local development environment. This allows you to easily
inspect, visualize, and troubleshoot your collaborative online experiences.

Our DevTools is currently only available if you are on Liveblocks 1.0 or later.
To view step-by-step instructions on how to upgrade your project to Liveblocks
1.0, check out our [upgrade guides](/docs/platform/upgrading/).

<Banner title="Live demo video">

If you want to see how this works in action, check out this
[live demo](https://www.youtube.com/watch?v=-6Jxejp0iuw) recorded by
[Vincent](https://twitter.com/nvie) on YouTube.

</Banner>

## Install the browser extension

Our DevTools extension is available for all major web browsers:

- [Chrome](https://chrome.google.com/webstore/detail/liveblocks-devtools/iiagocfmmhknpdalddkbiejnfmbmlffk)
- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/liveblocks-devtools/)
- [Edge](https://microsoftedge.microsoft.com/addons/detail/liveblocks-devtools/hfecmmnilleegmjaegkjjklnjbgadikg)

Within the “Liveblocks” panel, you will see two panes: storage and presence. The
storage pane represents each object that you have defined in storage as a tree,
likewise, the presence pane represents each user that is connected to your
Liveblocks project.

## Trying our DevTools

### With our Starter Kit

The [Liveblocks Starter Kit](/starter-kit) is a great way to get started with
Liveblocks and learn how to use our DevTools.

<Figure>
  <video autoPlay loop muted playsInline>
    <source
      src="/assets/devtools/liveblocks-devtools-starter-kit.mp4"
      type="video/mp4"
    />
  </video>
</Figure>
To use the Starter Kit with DevTools, run the following command:

```bash
npx create-liveblocks-app@latest --next
```

You can then follow the guided prompts in the terminal to create your project.
For additional information on how to use the Starter Kit, see the
[Starter Kit guide](/docs/tools/nextjs-starter-kit).

Once you have completed the Starter Kit prompts and installed the extension, run
the Starter Kit with `npm run dev` and open your browser to `localhost:3000`.
You should authenticate, and then create a new draft document. Once you have
created a new document, open up the developer tools window. Find the newly
available “Liveblocks” panel there.

<Figure>
  <video autoPlay loop muted playsInline>
    <source
      src="/assets/devtools/liveblocks-starter-dev.mp4"
      type="video/mp4"
    />
  </video>
</Figure>

### With an example from our gallery

All of [our examples](/examples) hosted on Liveblocks.io are available for you
to run locally from the
[examples directory](https://github.com/liveblocks/liveblocks/tree/main/examples)
in the Liveblocks repo. For testing and reviewing how to use our DevTools, we
will use the
[Advanced Collaborative Spreadsheet](https://liveblocks.io/examples/collaborative-spreadsheet-advanced/nextjs).

To download this example, run the following command, and follow the guided
prompts to create your project:

```bash
npx create-liveblocks-app@latest --example nextjs-spreadsheet-advanced
```

Once you have completed the prompts and installed the extension, run the example
with `npm run dev` and open your browser to `localhost:3000`.

<Figure highlight={false}>
  <Image
    src="/assets/devtools/spreadsheet-example.png"
    alt="Liveblocks DevTools panel"
    width={768}
    height={576}
  />
</Figure>

Open your browser and open up the developer tools window. Find the newly
available “Liveblocks” panel there, and you should see the values from the
spreadsheet examples populating storage.

## Features and tips

### Highlighting

When a value in storage changes, the DevTools will highlight the value to
indicate that it has changed.

<Figure>
  <video autoPlay loop muted playsInline>
    <source src="/assets/devtools/highlight-changes.mp4" type="video/mp4" />
  </video>
</Figure>

### Search

Storage values are searchable by key and regex.

<Figure>
  <video autoPlay loop muted playsInline>
    <source src="/assets/devtools/search-regex.mp4" type="video/mp4" />
  </video>
</Figure>

### Hover

Hovering over a value in storage or presence makes an eye icon appear. If
clicked, it displays the value of that key.

<Figure>
  <video autoPlay loop muted playsInline>
    <source src="/assets/devtools/expand-tooltip.mp4" type="video/mp4" />
  </video>
</Figure>

### Expand and collapse

- You can expand and collapse objects with one level of depth by using the arrow
  keys or spacebar
- You can quickly expand and collapse nested objects within storage by holding
  the alt/option key and clicking on the object

<Banner title="Initialization">

If you open the DevTools after a Liveblocks client has already been initialized,
you may see an empty page. This is because our DevTools is only able to retrieve
the current state of storage and presence. To fix this, you can refresh the page
or open the DevTools before initializing the Liveblocks client.

</Banner>

---
meta:
  title: "MCP server"
  parentTitle: "Tools"
  description:
    "Learn to use the official MCP server for Liveblocks, helpful for inspecting
    and modifying your project with AI in Cursor, VS Code, Claude Desktop, and
    more."
---

The Liveblocks MCP server allows you to inspect and modify your project with AI
in compatible apps such as Cursor, VS Code, Claude Desktop, and more.

<Figure>
  <video autoPlay loop muted playsInline>
    <source src="/assets/ai-copilots/mcp-server.mp4" type="video/mp4" />
  </video>
</Figure>

## Features

Our MCP server features 39 different tools, enabling most features from our
[REST API](/docs/api-reference/rest-api-endpoints). You can use it to:

- Create and modify rooms, threads, comments, notifications, more.
- Read realtime Storage and Yjs values.
- Broadcast custom events to connected clients.
- Mark threads and notifications as read/unread.

AI will often string tool calls together—for example, if you ask it to fetch a
room that doesn’t exist, it will ask if you’d like to create the room first, and
then fetch it after.

## Automatic setup

To install automatically, use one of the following commands, making sure to
insert a secret key from your correct project in [your dashboard](/dashboard).

<Banner type="warning" title="Only for development">

Don’t insert a secret key from your product application, as AI will have direct
access to modify it.

</Banner>

### Cursor

Run the following command to set up [Cursor](https://cursor.sh/):

```bash
npx -y @smithery/cli install @liveblocks/liveblocks-mcp-server --client cursor --key "{{SECRET_KEY}}"
```

### Claude

Run the following command to set up
[Claude Desktop](https://claude.ai/download):

```bash
npx -y @smithery/cli install @liveblocks/liveblocks-mcp-server --client claude --key "{{SECRET_KEY}}"
```

### VS Code

Run the following command to set up [VS Code](https://code.visualstudio.com/):

```bash
npx -y @smithery/cli install @liveblocks/liveblocks-mcp-server --client vscode --key "{{SECRET_KEY}}"
```

### Other clients

Find installation information for other clients on
[Smithery](https://smithery.ai/server/@liveblocks/liveblocks-mcp-server).

## Manual setup

1. Clone the
   [Liveblocks MCP server](https://github.com/liveblocks/liveblocks-mcp-server)
   repo.

```bash
git clone https://github.com/liveblocks/liveblocks-mcp-server.git
```

2. Build the project.

```bash
npm install
npm run build
```

### Cursor

3. Go to File → Cursor Settings → MCP → Add new server.

4. Add the following, with the full path to the repo and the correct secret key
   from [your dashboard](/dashboard):

```json
{
  "mcpServers": {
    "liveblocks-mcp-server": {
      "command": "node",
      "args": ["/full/path/to/the/repo/liveblocks-mcp-server/build/index.js"],
      "env": {
        "LIVEBLOCKS_SECRET_KEY": "{{SECRET_KEY}}"
      }
    }
  }
}
```

5. Check it's enabled in the MCP menu.

### Claude Desktop

3. Go to File → Settings → Developer → Edit Config.

4. Open the JSON file, `claude_desktop_config.json`.

5. Add the following, with the full path to the repo and the correct secret key
   from [your dashboard](/dashboard):

```json
{
  "mcpServers": {
    "liveblocks-mcp-server": {
      "command": "node",
      "args": ["/full/path/to/the/repo/liveblocks-mcp-server/build/index.js"],
      "env": {
        "LIVEBLOCKS_SECRET_KEY": "{{SECRET_KEY}}"
      }
    }
  }
}
```

## Source code

The source code is available in our
[GitHub repository](https://github.com/liveblocks/liveblocks-mcp-server).

---
meta:
  title: "Next.js Starter Kit"
  parentTitle: "Tools"
  description:
    "Kickstart your collaborative SaaS product with the Liveblocks Next.js
    Starter Kit."
---

The Next.js Starter Kit is an open-source template that showcases all aspects of
Liveblocks in a single, modern application.

<Figure>
  <video autoPlay loop muted playsInline>
    <source src="/assets/nextjs-starter-kit/starter-kit.mp4" type="video/mp4" />
  </video>
</Figure>

<Banner title="Live demo">

If you’d like to see how this works, make sure to
[check out this live demo video](https://www.youtube.com/watch?v=Lf7HQ4Z_Ovc) on
YouTube, or [try the project online](https://nextjs-starter-kit.liveblocks.app).

</Banner>

## Features

The Next.js Starter Kit includes the following

- Documents dashboard with pagination, drafts, groups, auto-revalidation
- Collaborative whiteboard app with a fully-featured share menu
- Authentication compatible with GitHub, Google, Auth0, and more
- Document permissions can be scoped to users, groups, and the public

## Set up the Next.js Starter Kit

### create-liveblocks-app

You can get started by running the following command:

```bash
npx create-liveblocks-app@latest --next
```

This will run an installer that allows you to download, configure, and deploy,
your project:

<Figure>
  <video autoPlay loop muted playsInline>
    <source
      src="/assets/nextjs-starter-kit/create-liveblocks-app.mp4"
      type="video/mp4"
    />
  </video>
</Figure>

Give your project a name, and select the [authentication](#authentication)
method you’d like to use in your app. If you are trying to generate a proof of
concept quickly, opting to use demo authentication may be a great option—you can
still add other authentication providers later on.

#### Deploy with Vercel Integration

If you would like to set up CI/CD with your application, we’ve made that process
straightforward for you as well. When prompted, you can select "deploy to
Vercel" to enable building and deploying the starter kit. The Vercel Integration
will open in a new browser window for you to complete the process of adding your
repository.

<Figure>
  <video autoPlay loop muted playsInline>
    <source
      src="/assets/nextjs-starter-kit/deploy-on-vercel.mp4"
      type="video/mp4"
    />
  </video>
</Figure>

If by clicking "Create" you receive "An unexpected internal error occurred" you
should validate within your code hosting platform of choice (GitHub, GitLab,
Bitbucket) that the Vercel integration has permission to access your repository.
In GitHub, permissions can be found can be found under Settings > Integrations >
Vercel.

#### Connect to Liveblocks and retrieve your secret key

If you prefer to work locally, you can tell the installer you would not like to
"deploy to Vercel." After declining the deployment option, the installer will
prompt adding your Liveblocks Key to the application automatically. If you
forwent deployment or indicated that you would like to get your Liveblocks
secret key automatically, the Liveblocks integration page will open in a new
browser tab. Once the integration page appears, you can sign up/sign in to
Liveblocks, create a new project, and import your API key into the installer.

<Figure>
  <video autoPlay loop muted playsInline>
    <source
      src="/assets/nextjs-starter-kit/create-liveblocks-project.mp4"
      type="video/mp4"
    />
  </video>
</Figure>

After finishing up, check the installer and follow the commands recommended to
get you started.

### Authentication [#authentication]

The Liveblocks starter kit uses [NextAuth.js](https://next-auth.js.org/) for
authentication, meaning many authentication providers can be configured with
minimal code changes. A demo authentication system is used by default, but it’s
easy to add real providers, such as GitHub, Auth0, and more.

Take a look at the guide for your chosen authentication method:

- [GitHub authentication](#github-authentication)
- [Auth0 authentication](#auth0-authentication)
- [Demo authentication](#demo-authentication)

### GitHub authentication [#github-authentication]

To use GitHub auth, make sure you selected "GitHub authentication" when running
the installer (or you’ve set up the
[provider manually](#add-multiple-authentication-providers)). This is how to set
up your GitHub secret key and client id.

1. Go to [Developer Settings](https://github.com/settings/apps) on GitHub and
   click "New GitHub App".
2. Enter an app name (e.g. `Liveblocks Starter Kit (dev)`). You’ll need a new
   app for each environment, so it’s helpful to place "dev" in the name.
3. Add a homepage URL—this isn’t important now, so a placeholder will do.
4. Find the "Callback URL" input just below, and add your local development URL
   (e.g. `http://localhost:3000`).
5. Look for the "Webhook" section and make sure to uncheck "Active".
6. Use the remaining default settings and press "Create GitHub App".

<Figure>
  <video autoPlay loop muted playsInline>
    <source src="/assets/nextjs-starter-kit/github-auth.mp4" type="video/mp4" />
  </video>
</Figure>

7. On the next page under "Client secrets", press the "Generate a new client
   secret" button (note that this is different to generating a private key!).
8. Copy this secret into the `/.env.local` file as `GITHUB_CLIENT_SECRET`
9. Go back to the previous page and find the "Client ID" near the top. Copy this
   into your `.env.local` file as `GITHUB_CLIENT_ID`

Almost there! `.env.local` should now contain lines similar to this:

```dotenv file=".env.local"
GITHUB_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
GITHUB_CLIENT_ID=XXXXXXXXXXXXXXXXXXXX
```

GitHub authentication is now complete! Next,
[add yourself as a user](#how-to-sign-up) to test out your authentication.

### Auth0 authentication [#auth0-authentication]

To use Auth0 auth, make sure you selected "Auth0 authentication" when running
the installer (or you’ve set up the
[provider manually](#add-multiple-authentication-providers)). This is how to set
up your Auth0 secret key and client information.

1. Go to your [Auth0 Dashboard](https://manage.auth0.com/dashboard) and click
   "Create Application".
2. Enter an app name (e.g. `Liveblocks Starter Kit (dev)`). You’ll need a new
   app for each environment, so it’s helpful to place "dev" in the name.
3. Select "Single Page Web Applications", and press "Create".
4. Copy your "Client ID" from the top of the page, and place it within
   `.env.local` as `AUTH0_CLIENT_ID`.
5. Click the "Settings" tab— we’ll be making a number of changes here.
6. Find the "Client Secret" input field, and copy the value into `.env.local` as
   `AUTH0_CLIENT_SECRET`.
7. Copy your "Domain" from the input field, add "https://" to the start, and
   place it within `.env.local` as `AUTH0_ISSUER_BASE_URL`.
8. Add the following to the "Allowed Callback URLs" textarea:
   `http://localhost:3000/api/auth/callback/auth0`.
9. Add the following to the "Allowed Logout URLs" textarea:
   `http://localhost:3000`.
10. Scroll to the bottom and press "Save changes".

<Figure>
  <video autoPlay loop muted playsInline>
    <source src="/assets/nextjs-starter-kit/auth0-auth.mp4" type="video/mp4" />
  </video>
</Figure>

`.env.local` should now contain these three lines, along with anything
previously there:

```dotenv file=".env.local"
AUTH0_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
AUTH0_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
AUTH0_ISSUER_BASE_URL=https://XXXXXXXXXXXXXXXXXX.com
```

Auth0 authentication is now set up! Next, [add yourself a user](#how-to-sign-up)
to test out your authentication.

### Demo authentication [#demo-authentication]

For quickly testing out your app, the demo authentication method can be used.
This method uses a NextAuth
[`CredentialsProvider`](https://next-auth.js.org/providers/credentials) to
simulate a real sign in system. To replace it with your own authentication
method, you can add any other NextAuth
[`Provider`](https://next-auth.js.org/providers/) to
[`/auth.config.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/auth.config.ts).

Read the [next section](#how-to-sign-up) to learn how to add a new user to your
demo application.

### How to sign up - add yourself as a user [#how-to-sign-up]

We haven’t set up a database, so we’re temporarily using the
[`/data`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/data)
folder instead. Before any user can sign in, they need to be added to
[`/data/users.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/data/users.ts).
Navigate there and add your details, for example, if you’re signing in with
`yourname@example.com`:

```ts file="data/users.ts"
{
  id: "yourname@example.com",
  name: "Your Name",
  avatar: "https://liveblocks.io/avatars/avatar-0.png",
  groupIds: ["product", "engineering", "design"],
},
```

Note that this is replacing the sign-up process, so you must enter the email of
any new user. If, for example, you’re using GitHub authentication, you can enter
the email address of any valid GitHub account.

### Ready to go

The Next.js Starter Kit is now ready to use! After setting up authentication,
make sure to restart the dev server to see your authentication in action.

```bash
npm run dev
```

## Assorted info

### Structure

The
[starter kit](https://github.com/liveblocks/liveblocks/tree/main/starter-kits/nextjs-starter-kit)
has the following structure:

```
app
components
data
icons
layouts
lib
 └─ actions
 └─ database
 └─ hooks
 └─ utils
primitives
styles
types
auth.ts
auth.config.ts
liveblocks.config.ts
liveblocks.server.config.ts
package.json
```

The important thing to note is that there are primitives, which are base level
components such as buttons (think of these as the small building blocks of a
page), components, which you can think of as multiplayer implementations
(cursors, badges, etc), app, which renders document level experiences and a
library (lib) of both server side and client side methods which contain the
logic to create and modify documents.

<br />
<details>
  <summary>More info about the structure</summary>
  <br />
  <dl>
    <dt>`/app/`</dt>
    <dd>Next.js app folder.</dd>
    <dt>`/components/`</dt>
    <dd>Components used in the app.</dd>
    <dt>`/data/`</dt>
    <dd>A demo database.</dd>
    <dt>`/icons/`</dt>
    <dd>Every icon component.</dd>
    <dt>`/layouts/`</dt>
    <dd>Page layout components.</dd>
    <dt>`/lib/actions/`</dt>
    <dd>
      Server actions that can be used on both client and server, used for
      accessing and modifying documents.
    </dd>
    <dt>`/lib/database/`</dt>
    <dd>Server functions used for getting database information.</dd>
    <dt>`/lib/hooks/`</dt>
    <dd>A set of React hooks that are used on the client.</dd>
    <dt>`/lib/utils/`</dt>
    <dd>Assorted document-related utilities.</dd>
    <dt>`/primitives/`</dt>
    <dd>Generic reusable components.</dd>
    <dt>`/types/`</dt>
    <dd>All reusable TypeScript types.</dd>
    <dt>`/auth.ts`</dt>
    <dd>File that links NextAuth to your database.</dd>
    <dt>`/auth.config.ts`</dt>
    <dt>File where NextAuth providers are set up.</dt>
    <dt>`/liveblocks.config.ts`</dt>
    <dd>Liveblocks config file.</dd>
    <dt>`/liveblocks.server.config.ts`</dt>
    <dd>Server-side Liveblocks config file.</dd>
  </dl>
</details>

### Async fetching and error handling

This starter kit makes extensive use of the following programming pattern for
fetching async resources:

```ts
const { data, error } = await getDocument({
  /* ... */
});

// An error has occured
if (error) {
  // { code: 400, message: "Document not found", suggestion: "Please check the URL is..." }
  console.log(error);
  return;
}

// Success, but result is empty
if (!data) {
  return;
}

// Success
// { name: "my-document", id: "hIas7GuihgHF8Fhv8Sskg",  ... }
console.log(data);
```

### Document functions

Much of the starter kit’s power is in the
[`/lib/actions`](https://github.com/liveblocks/liveblocks/tree/main/starter-kits/nextjs-starter-kit/lib/actions)
directory. The functions in these files allow you to edit your documents easily
and return type-safe objects. For example in an API endpoint:

```ts
import { createDocument } from "@/lib/actions";

export async function POST() {
  // Create a new document
  const { data, error } = await createDocument({
    name: "My document",
    type: "whiteboard",
    userId: "charlie.layne@example.com",
  });

  // ...
}
```

Because these functions are server actions, they can be leveraged on both client
and server.

```tsx
import { createDocument } from "@/lib/actions";

export function CreateDocumentButton() {
  async function handleCreateDocument() {
    // Create a new document
    const { data, error } = await createDocument({
      name: "My document",
      type: "whiteboard",
      userId: "charlie.layne@example.com",
    });
  }

  return <button onClick={handleCreateDocument}>New document</button>;
}
```

Functions that return data can be used with [SWR hooks](https://swr.vercel.app/)
that automatically update your data in components. For example,
`getDocumentUsers` returns a list of users with access to the room:

```tsx
// Convert from this
const { data, error } = await getDocumentUsers({
  documentId: "my-document-id",
});

// To this
const { data, error } = useDocumentsFunctionSWR([
  getDocumentUsers,
  {
    documentId: "my-document-id",
  },
]);
```

Here’s a working example:

```tsx
import { getDocumentUsers, useDocumentsFunctionSWR } from "../../lib/client";

export function ListUsers() {
  // Get users attached to a document and update every 1000ms
  const { data: users, error: usersError } = useDocumentsFunctionSWR(
    [
      getDocumentUsers,
      {
        documentId: "my-document-id",
      },
    ],
    { refreshInterval: 1000 }
  );

  if (usersError) {
    return <div>Error</div>;
  }

  if (!users) {
    return <div>Loading...</div>;
  }

  return (
    <ul>
      {users.map((user) => (
        <li>user.name</li>
      ))}
    </ul>
  );
}
```

### How to extend the Document type

If you’d like to add a new property to `Document`, it’s simple. First, edit the
`Document` type in
[`/types/documents.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/types/document.ts):

```ts file="types/documents.ts"
export type Document = {
  // Your new property
  randomNumber: number;
  //...
};
```

Then modify the return value in
[`/lib/utils/buildDocuments.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/utils/buildDocuments.ts).
This is a function that converts a Liveblocks room into your custom document
format:

```ts file="lib/utils/buildDocuments.ts"
// Return our custom Document format
const document: Document = {
  randomNumber: Math.random(),
  // ...
};
```

Next, run the following command to check for problems:

```bash
npm run typecheck
```

If no errors are returned, the document properties were successfully extended.

### How to extend the User & Session type

Similar to the way we extend the `Document` type, we can also extend the `User`
and `Session` type.

Adding a new property to `User`/`Session` is simple. First, edit the `User` type
in
[`/types/data.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/types/data.ts).

```ts file="types/data.ts"
export type User = {
  // Your new property
  randomNumber: number;
  // ...
};
```

Then make sure to return this new property in
[`/lib/database/getUser.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/database/getUser.ts).

```ts file="lib/database/getUser.ts"
return { randomNumber: Math.random() /* ... */ };
```

The new property will now be available to use in your app:

```ts
// randomNumber: Math.random(),
console.log(session.user.info.randomNumber);
```

#### Adding this to your Liveblocks app (optional)

Liveblocks presence is a way of displaying online presence between users,
helpful for live avatars, realtime cursors, etc., and it’s possible to attach a
properties to it for each user. To make a property accessible in presence (and
within the React hooks used in whiteboard), you must modify `UserInfo` in
[`/liveblocks.config.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/liveblocks.config.ts).

```ts file="liveblocks.config.ts"
export type UserInfo = Pick<User, "randomNumber" /* ... */>;
```

In this example
[Pick](https://www.typescriptlang.org/docs/handbook/utility-types.html) creates
the UserInfo type based off of the User type and adds additional keys based on
the properties you provide. After this, modify
[`/lib/actions/authorizeLiveblocks.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/actions/authorizeLiveblocks.ts).
First, we’ll give an anonymous user a property:

```ts file="lib/actions/authorizeLiveblocks.ts"
// Anonymous user info
const anonymousUser: User = {
  randomNumber: Math.random(),
  // ...
};
```

Next, we’ll get the signed-in user’s property:

```ts file="lib/actions/authorizeLiveblocks.ts"
// Get current user info from session (defined in /auth.config.ts)
// If no session found, this is a logged out/anonymous user
const {
  randomNumber,
  // ...
} = session?.user.info ?? anonymousUser;
```

And then pass this info to `authorize`:

```ts file="lib/actions/authorizeLiveblocks.ts"
// Get Liveblocks access token
const { data, error } = await authorize({
  userInfo: { randomNumber /* ... */ },
  // ...
});
```

To make sure to check everything’s hooked up correctly:

```bash
npm run typecheck
```

Once that’s working, the new property can then be used in your app:

```tsx
// My random number
const myRandomNumber = useSelf((me) => me.info.randomNumber);

// An array of everyone else’s random numbers
const everyonesRandomNumbers = useOthersMapped(
  (other) => other.info.randomNumber
);
```

### Adding multiple authentication providers [#add-multiple-authentication-providers]

It’s possible to add multiple authentication providers to the starter kit using
[NextAuth Providers](https://next-auth.js.org/providers/). Open
[`/auth.config.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/auth.config.ts)
and place your providers in the object:

```ts
import GithubProvider from "next-auth/providers/github";
import Auth0Provider from "next-auth/providers/auth0";

export const authOptions = {
  // ...
  providers: {
    GithubProvider({
      clientId: process.env.GITHUB_CLIENT_ID as string,
      clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
    }),
    Auth0Provider({
      clientId: process.env.AUTH0_CLIENT_ID as string,
      clientSecret: process.env.AUTH0_CLIENT_SECRET as string,
      issuer: process.env.AUTH0_ISSUER_BASE_URL as string,
    }),
  }
};
```

It’s not only possible with GitHub and Auth0, any
[NextAuth provider](https://next-auth.js.org/providers/) will work, such as
Google, X, Reddit, or more. You can find more information about getting the
necessary secrets on the NextAuth documentation, or on the provider’s website.

Note that if you’re using `CredentialsProvider` (for example, as used in the
demo authentication), `CredentialsProvider` must be removed before any other
authentication methods will appear.

### Switching themes

The starter kit comes with both a dark mode and light mode. By default, the user
sees the theme that corresponds to their system setting, but it’s easy to switch
your whole app to just dark or light mode by modifying
[`styles/globals.css`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/styles/globals.css).

- To use only light mode, remove the entire
  `@media (prefers-color-scheme: dark)` media query.
- To use only dark mode, copy the "Dark mode" CSS variables into the "Light
  mode" section, then remove the entire `@media (prefers-color-scheme: dark)`
  media query.

### Adding a database

To add a database you need to modify the following async functions to return the
correct properties:

- [`/lib/database/getGroup.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/database/getGroup.ts)
- [`/lib/database/getGroups.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/database/getGroups.ts)
- [`/lib/database/getUser.ts`](https://github.com/liveblocks/liveblocks/blob/main/starter-kits/nextjs-starter-kit/lib/database/getUser.ts)

You can then remove the `/data` folder. Everything else should work as expected.

---
meta:
  title: "Upgrading Liveblocks"
  description: "Select the Liveblocks version you’d like to update to"
---

Select the version you’d like to update to. When upgrading, please note that all
packages should be upgraded to the same version. If a version isn’t mentioned,
there are no breaking changes.

<ListGrid>
  <DocsCard
    type="image"
    title="Upgrading to 3.10"
    href="/docs/platform/upgrading/3.10"
    visual={<DocsCardGradientText>3.10</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 3.0"
    href="/docs/platform/upgrading/3.0"
    visual={<DocsCardGradientText>3.0</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 2.24"
    href="/docs/platform/upgrading/2.24"
    visual={<DocsCardGradientText>2.24</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 2.16"
    href="/docs/platform/upgrading/2.16"
    visual={<DocsCardGradientText>2.16</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 2.15"
    href="/docs/platform/upgrading/2.15"
    visual={<DocsCardGradientText>2.15</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 2.9"
    href="/docs/platform/upgrading/2.9"
    visual={<DocsCardGradientText>2.9</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 2.8"
    href="/docs/platform/upgrading/2.8"
    visual={<DocsCardGradientText>2.8</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 2.2"
    href="/docs/platform/upgrading/2.2"
    visual={<DocsCardGradientText>2.2</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 2.0"
    href="/docs/platform/upgrading/2.0"
    visual={<DocsCardGradientText>2.0</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 1.10"
    href="/docs/platform/upgrading/1.10"
    visual={<DocsCardGradientText>1.10</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 1.9"
    href="/docs/platform/upgrading/1.9"
    visual={<DocsCardGradientText>1.9</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 1.5"
    href="/docs/platform/upgrading/1.5"
    visual={<DocsCardGradientText>1.5</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 1.2"
    href="/docs/platform/upgrading/1.2"
    visual={<DocsCardGradientText>1.2</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 1.0"
    href="/docs/platform/upgrading/1.0"
    visual={<DocsCardGradientText>1.0</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 0.19"
    href="/docs/platform/upgrading/0.19"
    visual={<DocsCardGradientText>0.19</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 0.18"
    href="/docs/platform/upgrading/0.18"
    visual={<DocsCardGradientText>0.18</DocsCardGradientText>}
  />
  <DocsCard
    type="image"
    title="Upgrading to 0.17"
    href="/docs/platform/upgrading/0.17"
    visual={<DocsCardGradientText>0.17</DocsCardGradientText>}
  />
</ListGrid>

---
meta:
  title: "Upgrading to 0.17"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 0.17"
---

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

With the release of 0.17 we’re making a big investment in the stability and
reliability of Liveblocks. Our long term goal is to empower you to write and
help evolve your apps in the best way possible, and at enterprise scale. It
already was easy to get started with Liveblocks, and with these changes we want
to make _evolving_ your app just as easy.

The first step towards this goal is to take TypeScript support to the next level
for [`@liveblocks/client`][] and [`@liveblocks/react`][] (but our other packages
will soon follow suit). This will help you write code with confidence and catch
bugs as soon as possible in the development process. We have strictened our type
definitions to be more accurate and will recommend some new usage patterns, so
it will be easier for you to create _bug free_ collaborative apps.

To upgrade [`@liveblocks/client`][] and [`@liveblocks/react`][], run the
following command.

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

With these changes, we’re clearing the path to enable schema validation per
room, automatic data migrations, more powerful data selector APIs, and other
enterprise-level features.

Let’s dive in and take a look!

## Changes in @liveblocks/react [#react]

### Lifting up your state to the room level [#lifting-state-to-room]

With 0.16, it was possible to initialize a storage key with [`useObject`][],
[`useList`][], [`useMap`][].

Even if handy, we realized that it introduced confusion and unpredictable
behavior for most users. Imagine a scenario where you have two components
initializing the same storage key.

```tsx
function ComponentA() {
  const author = useObject("author", {
    firstName: "Ada",
    lastName: "Lovelace",
  });

  /* ... */
}

function ComponentB() {
  const author = useObject("author", {
    firstName: "Margaret",
    lastName: "Hamilton",
  });

  /* ... */
}
```

Depending on which component renders first, `author` will be Margaret Hamilton
or Ada Lovelace. To make this more predictable, we’re deprecating this and
recommend initializing the storage at the [`RoomProvider`][] level.

#### Before ❌ [@hidden]

```tsx
function ComponentA() {
  const author = useObject(
    "author",

    // ⚠️ Don’t initialize your data here anymore!
    { firstName: "Ada", lastName: "Lovelace" }
  );

  /* ... */
}
```

#### After ✅ [@hidden]

```tsx highlight="5-10,13,20"
import { LiveObject } from "@liveblocks/client";

function Root() {
  // Instead, initialize it at the RoomProvider level to remove all ambiguity
  const initialStorage = {
    author: new LiveObject({
      firstName: "Ada",
      lastName: "Lovelace",
    }),
  };

  return (
    <RoomProvider id="my-room-id" initialStorage={initialStorage}>
      <ComponentA />
    </RoomProvider>
  );
}

function ComponentA() {
  const author = useObject("author");

  /* ... */
}
```

<Banner title="Explicitly specify your live structures now">

Notice the explicit use of LiveObject here now. Before, when using useObject (or
useList, useMap), the data you passed to initialize it would automatically get
wrapped in LiveObject for you. This seemed convenient at first, but it also
caused confusion about which Storage keys were Live structures and which ones
were “normal” values. By opting in to which keys you want to be Live structures,
you now get full control over your storage data.

</Banner>

If you run into issues with these new patterns and you need help, please
[let us know](https://github.com/liveblocks/liveblocks/discussions/359). We’re
here to help!

### A better way to annotate your own types [#react-better-type-annotations]

In 0.16, most of our hooks accepted generic parameters that let you explicitly
provide your own types. For example:

```tsx highlight="3-5,11,19"
import { useMyPresence, RoomProvider } from "@liveblocks/react";

type Presence = {
  cursor: { x: number; y: number };
};

function Root() {
  return (
    <RoomProvider
      id="my-room-id"
      initialPresence={{ cursor: { x: 100, y: 100 } }}
    >
      <Component />
    </RoomProvider>
  );
}

function Component() {
  const [myPresence] = useMyPresence<Presence>();

  const cursor = myPresence.cursor; // Valid

  /* ... */
}
```

One issue with this API was that there was no good way to make sure that
`RoomProvider.initialPresence` and [`useMyPresence`][] types remain
synchronized, as there was no inherent connection between these.

If we added a `color` property to the `Presence` type, it would still be missing
from the `initialPresence` at the `RoomProvider` level, and TypeScript would not
be able to catch that bug. The opposite would also fail; omitting the `cursor`
property on the `initialPresence` would break at runtime but TypeScript would
not be able to catch this issue for you!

Another issue is that there could be many places where you’d have to provide
those extra type annotations.

With 0.17, we’re fixing all of this!

To do so, we’re introducing a new API called [`createRoomContext`][]. It lets
you type your `RoomProvider` and make sure that all your hooks types are
synchronized with the `RoomProvider`. Besides this initial setup, you will no
longer have to provide any type annotations elsewhere anymore.

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

const client = createClient({
  /* client options */
});

type Presence = {
  cursor: { x: number; y: number };
};

// This is just to illustrate the API - read on for tips on where to put this!
const { RoomProvider, useMyPresence } = createRoomContext<Presence>(client);

function Root() {
  return (
    <RoomProvider
      id="my-room-id"
      initialPresence={{ cursor: { x: 100, y: 100 } }}
    >
      <Component />
    </RoomProvider>
  );
}

function Component() {
  const [myPresence] = useMyPresence();

  // We can now be sure that cursor is a valid property without any generic typed param
  const cursor = myPresence.cursor;

  /* ... */
}
```

As you can see, `createRoomContext` optionally takes type parameters that let
you specify the shape of your app’s data (by specifying your own `Presence`,
`Storage`, `UserMeta`, `Event` types). Depending on the complexity of your app,
you may only need to use one or more of these.

Take a look at these examples to better see how to use and configure it:

- Our live-cursors-chat example app uses
  [`Presence`](https://github.com/liveblocks/liveblocks/blob/a11e5744531a21201ae9f9f9ad4f1aa5e74e141e/examples/nextjs-live-cursors-chat/liveblocks.config.ts#L8-L25)
  only
- Our live-avatars example app uses
  [`UserMeta`](https://github.com/liveblocks/liveblocks/blob/a11e5744531a21201ae9f9f9ad4f1aa5e74e141e/examples/nextjs-live-avatars/liveblocks.config.ts#L8-L26)
  only
- Our logo-builder example app uses
  [`Presence`](https://github.com/liveblocks/liveblocks/blob/a11e5744531a21201ae9f9f9ad4f1aa5e74e141e/examples/nextjs-logo-builder/liveblocks.config.ts#L8-L31),
  [`Storage`](https://github.com/liveblocks/liveblocks/blob/a11e5744531a21201ae9f9f9ad4f1aa5e74e141e/examples/nextjs-logo-builder/liveblocks.config.ts#L8-L31),
  and
  [`UserMeta`](https://github.com/liveblocks/liveblocks/blob/a11e5744531a21201ae9f9f9ad4f1aa5e74e141e/examples/nextjs-logo-builder/liveblocks.config.ts#L8-L31)

### Recommended upgrade steps

To make this refactoring as easy as possible, follow the steps below.

#### Step 1 - Upgrade `@liveblocks/client` and `@liveblocks/react` [@hidden]

To upgrade [`@liveblocks/client`][] and [`@liveblocks/react`], run the following
command.

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

#### Step 2 - Create a new file called `liveblocks.config.ts` where you will create your Liveblocks client, provider and hooks and re-export them. [@hidden]

```tsx file="liveblocks.config.ts"
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";

const client = createClient({
  /* client options */
});

// Presence represents the properties that will exist on every User in the Room
// and that will automatically be kept in sync. Accessible through the
// `user.presence` property. Must be JSON-serializable.
type Presence = {
  // cursor: { x: number, y: number } | null,
  // ...
};

// Optionally, Storage represents the shared document that persists in the
// Room, even after all Users leave. Fields under Storage typically are
// LiveList, LiveMap, LiveObject instances, for which updates are
// automatically persisted and synced to all connected clients.
type Storage = {
  // author: LiveObject<{ firstName: string, lastName: string }>,
  // ...
};

// Optionally, UserMeta represents static/readonly metadata on each User, as
// provided by your own custom auth backend (if used). Useful for data that
// will not change during a session, like a User's name or avatar.
// type UserMeta = {
//   id?: string,  // Accessible through `user.id`
//   info?: Json,  // Accessible through `user.info`
// };

// Optionally, the type of custom events broadcasted and listened for in this
// room. Must be JSON-serializable.
// type RoomEvent = {};

export const {
  RoomProvider,
  useMyPresence,
  useObject,
  /* ...all the other hooks you’re using... */
} = createRoomContext<Presence, Storage /* UserMeta, RoomEvent */>(client);
```

#### Step 3 - Replace all the direct hook imports from `@liveblocks/react` by your path to `liveblocks.config.ts` and remove all generic params. [@hidden]

##### Before ❌ [@hidden]

```tsx
import { useMyPresence, useOthers, useObject } from "@liveblocks/react";
import { Author } from "./types";

type MyPresence = {
  cursor: { x: number; y: number } | null;
};

function Component() {
  const author = useObject<Author>("author");
  const [{ cursor }] = useMyPresence<MyPresence>();
  const others = useOthers<Presence>();
}
```

##### After ✅ [@hidden]

```tsx
import { useMyPresence, useOthers, useObject } from "./liveblocks.config";

function Component() {
  const author = useObject("author");
  const [{ cursor }] = useMyPresence();
  const others = useOthers();
}
```

#### Step 4 - Remove your [`LiveblocksProvider`][] at the top of your react tree. It’s not needed anymore! [@hidden]

##### Before ❌ [@hidden]

```tsx
import { createClient } from "@liveblocks/client";
import { LiveblocksProvider, RoomProvider } from "@liveblocks/react";

const client = createClient({
  /* ... */
});

ReactDOM.render(
  <LiveblocksProvider client={client}>
    <RoomProvider id="my-room-id">
      <App />
    </RoomProvider>
  </LiveblocksProvider>,
  document.getElementById("root")
);
```

##### After ✅ [@hidden]

```tsx
import { RoomProvider } from "./liveblocks.config";

ReactDOM.render(
  <RoomProvider id="my-room-id">
    <App />
  </RoomProvider>,
  document.getElementById("root")
);
```

If you run into issues with these new patterns and you need help, please
[let us know](https://github.com/liveblocks/liveblocks/discussions/360). We’re
here to help!

## Changes in @liveblocks/client [#client]

### Removed dangerous default type params [#no-more-default-type-params]

In 0.16, while [`LiveList`][], [`LiveMap`][], [`LiveObject`][] were generics,
they also took default type params, which made them a footgun. It was easy to
accidentally use them in a way that would discard useful type information and
hinder inference.

For example:

```ts
// ✅ Inferred
let list = new LiveList([1, 2, 3]);

// ✅ Explicit
let list: LiveList<number> = new LiveList([1, 2, 3]);

// ☢️ 0.16: Dangerous footgun: discards useful type information!
// 🚫 0.17: No longer possible
let list: LiveList = new LiveList([1, 2, 3]);
```

They now mimic their equivalent built-in TypeScript generics, so:

- `LiveList<T>` is now just like `Array<T>`
- `LiveMap<K, V>` is now just like `Map<K, V>`
- `LiveObject<{ a: number, b: string }>` is now just like
  `{ a: number, b: string }`

### No longer import Presence [#define-dont-import-presence]

In 0.16, we exposed a `Presence` type that you could import, which was just an
alias for “any JSON object”—not that useful! Importing Presence from Liveblocks
made no sense. By definition, Presence is data owned and defined by your
application after all.

You should no longer need to _import_ this type.

```tsx
// ❌ No longer need to _import_ Presence
import type { Presence } from "@liveblocks/client";

client.enter<Presence>("myRoom");
```

Instead, just _define_ it:

```tsx
// ✅ Just define the shape your app needs
type Presence = {
  cursor: { x: number; y: number } | null;
};

client.enter<Presence>("myRoom");
```

### A better way to annotate your own types [#client-better-type-annotations]

We already talked about why, in React, we
[improved the way you can annotate your own app’s data](#react-better-type-annotations),
by annotating the types only once, at the “top” of your app.

For the same reason, we’re doing a similar thing in the client package.

<Banner title="You may not need this">

This section only applies if you’re not using the React package.

</Banner>

```tsx highlight="18-21,24"
import { createClient } from "@liveblocks/client";
import { Author } from "./types";

const client = createClient({
  /* client options */
});

type Presence = {
  cursor: { x: number; y: number } | null;
};

type Storage = {
  author: LiveObject<Author>;
};

// ❌ In 0.16, you had to annotate each method separately
const room = client.enter("myRoom");
const { root } = await room.getStorage<Storage>();
const author = root.get<Author>("author");
const me = room.getPresence<Presence>();
const others = room.getOthers<Presence>();

// ✅ In 0.17, you can simply annotate it once, at the "top"
const room = client.enter<Presence, Storage>("myRoom");
const { root } = await room.getStorage();
const author = root.get("author");
const me = room.getPresence();
const others = room.getOthers();
```

If you run into issues with these new patterns and you need help, please let us
know. We’re here to help!

[`@liveblocks/client`]: /docs/api-reference/liveblocks-client
[`@liveblocks/react`]: /docs/api-reference/liveblocks-react
[`createroomcontext`]: /docs/api-reference/liveblocks-react#createRoomContext
[`liveblocksprovider`]: /docs/api-reference/liveblocks-react#LiveblocksProvider
[`livelist`]: /docs/api-reference/liveblocks-client#LiveList
[`livemap`]: /docs/api-reference/liveblocks-client#LiveMap
[`liveobject`]: /docs/api-reference/liveblocks-client#LiveObject
[`roomprovider`]: /docs/api-reference/liveblocks-react#RoomProvider
[`uselist`]: /docs/api-reference/liveblocks-react#useList
[`usemap`]: /docs/api-reference/liveblocks-react#useMap
[`usemypresence`]: /docs/api-reference/liveblocks-react#useMyPresence
[`useobject`]: /docs/api-reference/liveblocks-react#useObject

---
meta:
  title: "Upgrading to 0.18"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 0.18"
---

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

With the release of 0.18 we’re bringing some exciting and pretty major
improvements to our React hooks, letting you build apps with ease and with more
control over the exact behavior.

The new APIs we’re introducing here solve many subtle and not-so-subtle pain
points. We heard your feedback, and think we have shipped something awesome that
you’ll love.

This guide consists of two sections:

1. Introduction of the new APIs to get a taste of the new features
2. Recommended upgrade steps to get the most out of it for your app
   ([jump straight to it](#upgrade-steps))

Let’s dive right in!

## Changes in @liveblocks/react [#react]

With 0.18, the biggest conceptual shift is that our hooks to consume data from
Liveblocks now return normal JavaScript data structures (objects, arrays, maps)
that are _immutable_ by default.

Suppose you have initialized your room with:

```tsx highlight="5-9"
<RoomProvider
  id="my-room"
  initialPresence={/* ... */}
  initialStorage={{
    scientist: new LiveObject({
      firstName: "Ada",
      lastName: "Lovelace",
      pets: new LiveList(["🐶", "🐈"]),
    }),
  }}
>
  <App />
</RoomProvider>
```

### Accessing nested data [#nested-data]

Reading nested data from there is now much easier:

#### Previously ❌ [@hidden]

```tsx
function Component() {
  const scientist = useObject("scientist");
  if (scientist == null) {
    return null;
  }

  const pets = scientist.get("pets").toArray();
  // ["🐶", "🐈"]
}
```

#### Now ✅ [@hidden]

```tsx
function Component() {
  const pets = useStorage((root) => root.scientist.pets);
  // ["🐶", "🐈"]
}
```

As you can see, because we can read data with normal JavaScript data structures,
accessing nested data is now straightforward.

### Subscribing to updates is automatic now [#subscriptions]

Rerendering your components when nested data—like our scientist’s pets
list—changes was a true head breaker before. This required extra helper
components, manual subscriptions, and manual conversion to “normal” JavaScript
arrays.

Now, rerendering your component when data changes is automatic, even for deeply
nested data.

#### Previously ❌ [@hidden]

```tsx
function Component() {
  const scientist = useObject("scientist");
  if (scientist == null) {
    return null;
  }

  return <Pets livePets={scientist.get("pets")} />;
}

function Pets({ livePets }) {
  const room = useRoom();
  const [pets, setPets] = useState(livePets.toArray());

  useEffect(() => {
    return room.subscribe(livePets, () => {
      setPets(livePets.toArray());
    });
  }, [room, livePets]);
}
```

#### Now ✅ [@hidden]

```tsx
function Component() {
  const pets = useStorage((root) => root.scientist.pets);
  // ["🐶", "🐈"]
}
```

There is no typo in this example. This is the actual code.

### Multiple subscriptions are just as easy [#multi-subscriptions]

Previously, if you wanted to derive a computed value from multiple storage
values, it took some manual setup to ensure the component would automatically
rerender when either of those values changed.

Now, this is fully automatic. Or should we say, automagic?

#### Previously ❌ [@hidden]

```tsx highlight="11"
function Component() {
  const objA = useObject("a");
  const objB = useObject("b");

  const room = useRoom();
  const [sum, setSum] = useState();
  //     ^^^ We’re trying to compute the result of a.x + b.x in here

  useEffect(() => {
    function onChange() {
      setSum(objA.get("x") + objB.get("x"));
    }

    const unsubA = room.subscribe(objA, onChange);
    const unsubB = room.subscribe(objB, onChange);

    return () => {
      unsubA();
      unsubB();
    };
  }, [room, objA, objB]);
}
```

#### Now ✅ [@hidden]

```tsx
function Component() {
  const sum = useStorage((root) => root.a.x + root.b.x);
}
```

This component will rerender automatically any time `a.x` or `b.x` changes, but
not more often.

### Guaranteed referential equality [#referential-equality]

Previously we returned mutable Live structures for performance reasons because
converting live changing data to JavaScript data structures constantly (and
recursively) was previously too slow to do on every render. This led to
unintuitive behavior when used with React hooks dependencies.

Not anymore! Due to a technique called structural sharing, we’re now able to
_guarantee_ for nodes in the Storage tree that as long as their (direct or
nested) contents haven’t changed in Storage, their immutable representation will
remain to be the same object references on the next render. This means that you
can rely on referential equality, as you may have expected in the first place.

#### Previously ❌ [@hidden]

```tsx
function Component() {
  const scientist = useObject("scientist");

  useEffect(() => {
    // Effect never triggered when scientist (or their pets list) changes! :(
  }, [scientist]);
}
```

#### Now ✅ [@hidden]

```tsx
function Component() {
  const scientist = useStorage((root) => root.scientist);

  useEffect(() => {
    // Effect triggered every time scientist (or their pets list) changes! :)
    // But not more often than that!
  }, [scientist]);
}
```

### Suspense support

Starting with 0.18, all hooks that read data from Liveblocks come with a
Suspense version of the hook which will never return `null` to indicate the
“still loading” state. Instead, they will suspend the rendering of the component
tree until Liveblocks has finished loading.

We recommend you to adopt Suspense if you can because it lets you get rid of the
ugly `null` checks, helper components to “eat off” those null cases, and the
prop drilling that necessarily comes with all that.

#### Previously ❌ [@hidden]

```tsx
function Component() {
  const camera = useObject("camera");
  const items = useList("items");

  // 👎
  if (camera == null || items == null) {
    return <div>Still loading...</div>;
  }

  return <MyRealComponent camera={camera} items={items} />;
}
```

#### Now ✅ [@hidden]

Set up a Suspense boundary once:

```tsx file="App.tsx" highlight="6"
import { Suspense } from "react";

function Setup() {
  return (
    // Once
    <Suspense fallback={<Loading />}>
      <App />
    </Suspense>
  );
}
```

Switch to use the Suspense versions of our hooks instead of the “normal” ones:

```tsx file="liveblocks.config.ts" highlight="2"
export const {
  suspense: {
    RoomProvider,
    useStorage,
    /* etc. */
  },
} = createRoomContext(client);
```

Then, enjoy no more null checks everywhere in your app:

```tsx file="Component.tsx"
function Component() {
  const camera = useStorage((root) => root.camera);
  const items = useStorage((root) => root.items);
  // No more null checking! :)
}
```

## Recommended upgrade steps [#upgrade-steps]

To get the most out of the new hooks, we recommend following the steps below to
gradually upgrade your app to make use of the new hooks.

### Step 1: Install the latest package [@hidden]

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

### Step 2: Make sure you’re setting initial presence [@hidden]

We now require setting an initial presence value when you connect to a room
explicitly. This ensures that every user is guaranteed to always have a known
presence value.

Check that you have this in your config file:

```tsx file="liveblocks.config.ts" highlight="3"
<RoomProvider
  id="my-room"
  initialPresence={{}}
  //              ^^^^ No longer optional
>
  <App />
</RoomProvider>
```

If your app somehow doesn’t use Presence, you can just set an empty object
(`{}`) here.

### Step 3: You can remove some uncertainty from user instances [@hidden]

If you have expressions in your code that look like...

```tsx
user.info?.avatar;
//       ^
user.presence?.cursor.x;
//           ^
```

You can now remove these optional chainings. The fields `info` and `presence`
will now always be set on `User` instances.

### Step 4: Adopt Suspense (optional) [@hidden]

Now is a great moment to opt-in to Suspense (see the
[React docs](https://reactjs.org/docs/react-api.html#suspense)) with Liveblocks,
if you can or want to use it in your app. We recommend it for most apps because
it makes working with the new hooks even nicer.

To avoid repeating ourselves, please follow the instruction below.

<Banner title="Adopting Suspense">

Follow
[these instructions to adopt Suspense](/docs/api-reference/liveblocks-react#suspense).
Don’t worry, we’ll wait.

</Banner>

Now that you have updated your app to Suspense, you should be able to remove all
these pesky `null` checks from your code.

```tsx highlight="6-8"
function Component() {
  const a = useMap("a");
  const b = useList("b");
  const c = useObject("c");

  if (a == null || b == null || c == null) {
    return <Loading />;
  }

  /* ... */
}
```

Afterward, please verify that your app still works like normal.

### Step 5a: Replace reads with `useStorage` [@hidden]

We recommend rewriting all usages of `useList`, `useObject` and `useMap` if
those are used for _reading_ data only. If used only for reading values from
Storage, you could turn these into an equivalent `useStorage` call, which has
fewer gotchas.

For example, change:

```tsx
// ❌
const obj = useObject("a");
const list = useObject("b");
const map = useMap("c");
```

to:

```tsx
// ✅
const obj = useStorage((root) => root.a);
const list = useStorage((root) => root.b);
const map = useStorage((root) => root.c);
```

Note that the `root` argument you receive here is the immutable normal
JavaScript equivalent of your entire Storage tree, as returned by calling
[`.toImmutable`][].

So this means that if you have been manually converting the mutable Live
structures to normal data structures, you no longer have to do this:

```tsx
// ❌
obj.toObject();
list.toArray();
```

```tsx
// ✅
obj; // Already a normal JS object
list; // Already a normal JS array
```

Please note that `useList`, `useObject`, or `useMap` are not deprecated and
still work with the same behavior as before. We just no longer recommend their
use.

### Step 5b: Replace mutations with useMutation [@hidden]

If you are (also) using `useList`, `useObject` or `useMap` to obtain a mutable
reference to the Live structure to _mutate_ it, you can rewrite those use cases
to use the new `useMutation` hook instead.

For example:

```tsx
// ❌
const obj = useObject("a");

return (
  <input
    value={obj.get("name")}
    //         ^^^ Live object used for reading
    onClick={() => {
      obj.set("name", e.currentTarget.value);
      //  ^^^ Live object used for mutating
    }}
  />
);
```

The idiomatic way to deal with Storage is to _consume_ data using simple/normal
JS data structures and to _mutate_ data using a callback function that you can
create with `useMutation`, which provides access to the mutable Live structures.

```tsx
// ✅
const name = useStorage((root) => root.a.name);

const setName = useMutation(({ storage }, newName) => {
  storage.get("a").set("name", newName);
  //               ^^^ Mutation goes here
}, []);

return (
  <input
    value={name}
    //     ^^^^ Reading uses simple values
    onClick={(e) => setName(e.currentTarget.value)}
  />
);
```

Even though in this contrived example it may look more complicated, in large
apps this pattern will vastly simplify your app’s complexity.

<Banner title="Tip!">

See the [`useMutation`][] documentation to learn all the details, because it has
a lot more in store.

</Banner>

### Step 6: Get rid of `room.subscribe()` calls [@hidden]

Historically the only way to get full control over exactly when and how your
components would rerender was to use the low-level `room.subscribe()` API.

Most, if not all, of these use cases can be replaced by an equivalent, yet much
simpler call to `useStorage` with a selector function that does an equivalent
thing.

**Common use case: subscribing to nested data**  
If you are using `room.subscribe` to manually rerender components when nested
data changes, you can replace it by “just” selecting the nested fields you’re
interested in. See [this example](#subscriptions).

```tsx
// ✅ Automatically rerenders if pets changes (but not more often)
const nested = useStorage((root) => root.scientist.pets);
```

**Common use case: subscribing to a computed value**  
If you are using `room.subscribe` to synchronize a computation based on multiple
storage values, you can replace it by “just” doing the computation in See
[this example](#multi-subscriptions).

```tsx
// ✅ Automatically rerenders if computed value changes
const sum = useStorage((root) => root.a + root.b);
```

If you have another use case for `room.subscribe` that you think isn’t possible
to express in an equivalent `useStorage` call, please
[let us know about it](https://github.com/liveblocks/liveblocks/discussions/504).
We’re happy to help!

### Step 7: Get rid of manual batch calls [@hidden]

Most, if not all, cases of manually calling `useBatch` or `room.batch` should no
longer be needed and can be replaced by `useMutation`, which automatically
batches already!

That’s it!

If you run into issues with these new patterns and you need help, please
[let us know](https://github.com/liveblocks/liveblocks/discussions/504). We’re
here to help!

[`usemutation`]: /docs/api-reference/liveblocks-react#useMutation
[`.toimmutable`]: /docs/api-reference/liveblocks-client#LiveObject.toImmutable

---
meta:
  title: "Upgrading to 0.19"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 0.19"
---

In the Liveblocks 0.19 release, we’re adding support for Zustand v4 to
`@liveblocks/zustand`. Zustand v4 brings greatly improved TypeScript types to
its APIs, enabling us to enhance the quality of our types, bringing it in line
with our React package.

Let’s take a look!

## Upgrading steps by package

### @liveblocks/react [#react]

To update `@liveblocks/react` to 0.19, run the following command using your
preferred package manager:

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

#### useOther now requires a selector argument [@hidden]

`useOther` now requires a
[selector](/docs/api-reference/liveblocks-react#selectors-receive-immutable-data)
function argument. You will need to replace instances of `useOther` that do not
use a selector in your codebase.

#### Previously ❌ [@hidden]

```tsx
const other = useOther(id); // 👈 does not include a selector
```

#### Now ✅ [@hidden]

```tsx
const other = useOther(id, (other) => other); // 👈 requires a selector
```

#### Include unstable_batchedUpdates if you use React 17 [@hidden]

We’ve added support to prevent the stale props/zombie child scenario. To avoid
this issue, we enforce passing the `unstable_batchedUpdates` prop to
`RoomProvider`.

<Banner title="You may not need this">

This section only applies if you are using React 17 or lower. You do not need to
do anything if you are using React 18 or higher.

</Banner>

```tsx highlight="2,8"
// ⚠️ Only if you’re using React 17 or lower
import { unstable_batchedUpdates } from "react-dom"; // 👈
<RoomProvider
  id="my-room"
  initialPresence={...}
  initialStorage={...}
  unstable_batchedUpdates={unstable_batchedUpdates}
>
  <App />
</RoomProvider>
```

For additional context, see the
[troubleshooting guide](/docs/platform/troubleshooting#stale-props-zombie-child)

### @liveblocks/redux [#redux]

To update `@liveblocks/redux` to 0.19, run the following command using your
preferred package manager:

```bash
npm install @liveblocks/client@0.19 @liveblocks/redux@0.19
```

#### Update use of default export liveblocksEnhancer [@hidden]

The main export has been renamed, so you will need to update your imports and
use of the enhancer:

#### Previously ❌ [@hidden]

```ts
import { enhancer } from "@liveblocks/redux";
```

#### Now ✅ [@hidden]

```ts
import { liveblocksEnhancer } from "@liveblocks/redux";
```

#### Remove the second argument to state.liveblocks.enterRoom [@hidden]

When calling `state.liveblocks.enterRoom()`, you should not pass an explicit
initial state. It will use the state in your Redux store, for consistency and
ease of use. To migrate, make the following code changes:

#### Previously ❌ [@hidden]

```ts
useEffect(() => {
  enterRoom("room-id", {
    todos: [], // 👈 remove explicit initial state
  });
});
```

#### Now ✅ [@hidden]

```ts
useEffect(() => {
  enterRoom("room-id");
});
```

### @liveblocks/zustand [#zustand]

In 0.19 we added support for Zustand v4 (specifically v4.1.3 or higher) and will
no longer support Zustand v4.1.2 or lower. This is because Zustand v4.1.3 brings
greatly improved TypeScript types to its APIs, and consequently, we can improve
our internal types. To migrate, make the following code changes:

To update `@liveblocks/zustand` to 0.19, run the following command using your
preferred package manager:

```bash
npm install @liveblocks/client@0.19 @liveblocks/zustand@0.19
```

- Change these imports, if applicable, and rename accordingly:

#### Previously ❌ [@hidden]

```ts
import { middleware } from "@liveblocks/zustand";
import type { LiveblocksState } from "@liveblocks/zustand";
```

#### Now ✅ [@hidden]

```ts
import { liveblocks } from "@liveblocks/zustand";
import type { WithLiveblocks } from "@liveblocks/zustand";
```

- Update to the Zustand v4 recommended pattern:

#### Previously ❌ [@hidden]

```ts
create(liveblocks<MyState, ...>(...))
```

#### Now ✅ [@hidden]

```ts
create<WithLiveblocks<MyState, ...>>()(liveblocks(...))
```

To be clear:

1. First, move the type annotation away from the `liveblocks` middleware call,
   and onto the `create` call.
2. Next, wrap your `MyState` type in a `WithLiveblocks<...>` wrapper. This will
   make sure the injected `liveblocks` property on your Zustand state will be
   correctly typed.
3. Finally, make sure to add the extra call `()` wrapper, needed by Zustand v4
   now:
   ```ts
   create<WithLiveblocks<MyState, ...>>()(liveblocks(...))
   //                                  ^^ Not a typo
   ```

- Remove the second argument to `state.liveblocks.enterRoom()`: it no longer
  takes an explicit initial state. Instead, it’s automatically be populated from
  your Zustand state.

## Improvements [#improvements]

This release brings several changes to `@liveblocks/react`, which improve
rendering performance and stability. Additionally, we have refactored our
internal packages to increase code sharing. You can review the
[release notes](https://github.com/liveblocks/liveblocks/releases) for more
details.

### @liveblocks/react [#react-improvements]

#### New shouldInitiallyConnect prop [@hidden]

We added a new property `shouldInitiallyConnect` to `RoomProvider`, which lets
you control whether or not the room connects to Liveblock servers. By default,
it will check the `typeof window` to determine if it should connect. When using
SSR, you can set it to `false` to prevent the room from connecting to Liveblocks
servers.

```tsx highlight="4"
<RoomProvider
  id="room"
  initialPresence={{}}
  shouldInitiallyConnect={false} // 👈 control initial connection to Liveblocks
>
  <App />
</RoomProvider>
```

#### Addition of @liveblocks-core package [@hidden]

We restructured our internal packages to increase code sharing. You may notice a
new dependency in your dependency tree: `@liveblocks/core`. It contains private
APIs that aren’t intended for direct consumption.

### @liveblocks/client [#client-improvements]

#### New shouldInitiallyConnect option [@hidden]

Similar to the [`shouldInitiallyConnect`](#react-shouldInitiallyConnect) prop in
`@liveblocks/react`, we added a `shouldInitiallyConnect` option to
`client.enter`. The default value is `true`. You may want to set it to `false`
when using SSR, or if you would prefer to establish the WebSocket connection
later.

```tsx
client.enter("room", {
  initialPresence: {},
  shouldInitiallyConnect: false, // 👈 for SSR using the @liveblocks/client package
});
```

### @liveblocks/\* [#all]

All packages now provide an `isReadOnly` flag on user instances. It is available
by calling `getSelf()` and `getOthers`. `isReadOnly` is true when storage is
read-only, as well as when a user has `read` permission to the `room` and
`write` permission to `presence`.

```ts highlight="3,5"
const me = room.getSelf();

me.isReadOnly; // boolean

const others = room.getOthers();
for (const other of others) {
  other.isReadOnly; // boolean
}
```

You can learn more about room permissions under
[authentication](/docs/authentication).

That’s it!

If you run into issues with these new patterns and you need help, please let us
know [by email](mailto:support@liveblocks.io) or by joining our
[Discord community](/discord)! We’re here to help!

---
meta:
  title: "Upgrading to 1.0"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 1.0"
---

Liveblocks 1.0 is our first major release, marking the end of the beta period
for storage and conflict-free data types. This release includes improvements to
our pricing model that make billing more predictable. Additionally, we have
vastly improved the ability to animate cursors at up to 60fps by updating the
throttle option in `createClient`. The primary change you should note is the
transition to making the `userId` mandatory in the `authorize` option. This
change is in line with our [new pricing model](https://liveblocks.io/pricing).

Let’s take a look!

## Upgrading steps by package

### @liveblocks/node [#node]

To update `@liveblocks/node` to 1.0, run the following command using your
preferred package manager:

```bash
npm install @liveblocks/node@1.0.0
```

#### Update the authorize option [@hidden]

We have updated the `authorize` method to make `userId` mandatory. This change
ties into our new [pricing model](https://liveblocks.io/pricing), which is based
on Monthly Active Users (MAU) instead of connections. We use `userId` to track
MAU associated with a Liveblocks account.

```ts highlight="20"
import { authorize } from "@liveblocks/node";

// Replace this key with your secret key provided at
// https://liveblocks.io/dashboard/projects/{projectId}/apikeys
const secret = "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx";

export default async function auth(req, res) {
  /**
   * Implement your own security here.
   *
   * It’s your responsibility to ensure that the caller of this endpoint
   * is a valid user by validating the cookies or authentication headers
   * and that it has access to the requested room.
   */
  const room = req.body.room;
  const response = await authorize({
    room,
    secret,
    // Corresponds to the UserMeta[id] type defined in liveblocks.config.ts
    userId: "123", // Required
    groupIds: ["456"], // Optional
    userInfo: {
      // Optional, corresponds to the UserMeta[info] type defined in liveblocks.config.ts
      name: "Ada Lovelace",
      color: "red",
    },
  });
  return res.status(response.status).end(response.body);
}
```

If you do not use the `authorize` function because you are using the public API
key method, a cookie will be set in the browser when a user connects to a room.
This cookie will be used to track MAU associated with a Liveblocks account and
will expire after 30 days.

### Authorize endpoint [#authorize-endpoint]

If you do not use the `authorize` function, but call the `authorize` endpoint
with a secret key instead, you should pass the `userId` even though it is not
mandatory on the API level because the userID will be used for MAU. Set the
`Authorization: Bearer` header to your secret key.

```ts
POST https://api.liveblocks.io/v2/rooms/{roomId}/authorize
```

Set the following the request body:

```ts highlight="3"
{
  "userId": "user123",
  "groupIds": [
    "g1",
    "g2"
  ],
  "userInfo": {
    "name": "bob",
    "colors": [
      "blue",
      "red"
    ]
  }
}
```

## Improvements [#improvements]

We added additional features to [Webhooks](/docs/platform/webhooks),
enhancements to the `Room` class, and more. You can review the
[release notes](https://github.com/liveblocks/liveblocks/releases) for more
details.

### @liveblocks/client [#client-improvements]

To update `@liveblocks/client` to 1.0, run the following command using your
preferred package manager:

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

#### Improvements to throttle allow animation of up to 60fps [@hidden]

You can now specify the `throttle` option in
[`createClient`](https://liveblocks.io/docs/api-reference/liveblocks-client#createClientThrottle)
that can go as low as 16ms. If not set, the default value is 100ms.

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

const client = createClient({
  /* ... other options ... */
  throttle: 16,
});
```

You can see the marked improvement in the animation from 80ms (original limit)
to 16ms in this
[tweet](https://twitter.com/ctnicholasdev/status/1622656511758700546). If you
want to try it out for yourself, check out
[live cursors example](https://liveblocks.io/examples/live-cursors/nextjs).

#### New methods available on the Room class [@hidden]

We added new methods to the `Room` class that you can use to obtain the storage
status of a room, subscribe to storage status changes, or reconnect to a room
whenever necessary. You can also use the
[Liveblocks DevTools extension](/devtools) to visualize realtime changes to
storage while implementing these changes.

Use `room.getStorageStatus` to retrieve the storage status of a room. The
statuses are:

- `not-loaded`: Initial state when entering the room.
- `loading`: Once the storage has been requested via room.getStorage().
- `synchronizing`: When some local updates have not been acknowledged by
  Liveblocks servers.
- `synchronized`: Storage is in sync with Liveblocks servers.

Use `room.subscribe` to subscribe to storage status changes. This method returns
an unsubscribe function.

```ts
room.subscribe("storage-status", (status) => {
  // Implement your logic here
  switch (status) {
    case "not-loaded":
      break;
    case "loading":
      break;
    case "synchronizing":
      break;
    case "synchronized":
      break;
    default:
      break;
  }
});
```

You can use `room.reconnect()` to close the room connection and try to
[reconnect](/docs/api-reference/liveblocks-client#Room.reconnect).

### @liveblocks/node [#node-improvements]

#### Webhooks enhancements [@hidden]

We have added two new events to our Webhooks functionality: `RoomCreatedEvent`
and `RoomDeletedEvent`. We have also added a `WebhookHandler` class to make it
easier to verify event requests from Liveblocks’ Webhooks functionality. It also
provides fully typed `WebhookEvents`.

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

const webhookHandler = new WebhookHandler(process.env.SECRET);

const event = webhookHandler.verifyRequest({
  headers: req.headers,
  rawBody: req.body,
});
```

Verification of events is critical to ensure that the events are coming from
Liveblocks and not from a malicious source. Check out our
[Webhooks guide](https://liveblocks.io/docs/platform/webhooks) for more details.

That’s it!

If you have issues with these new patterns and need help, please let us know
[by email](mailto:support@liveblocks.io) or by joining our
[Discord community](/discord)! We’re here to help!

---
meta:
  title: "Upgrading to 1.10"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 1.10"
---

Notifications is the first Liveblocks product that works across multiple rooms,
and this has required us to introduce breaking changes to ensure a consistent
API for Comments and Notifications.

## How to upgrade? [#how]

You can upgrade to 1.10 by downloading the latest version of each Liveblocks
package you’re using, for example in a React app:

```bash
npm install @liveblocks/client@latest @liveblocks/react@latest @liveblocks/react-ui@latest @liveblocks/node@latest
```

<Banner title="Update every Liveblocks package">

If you’re using any other Liveblocks packages make sure to update those too.

</Banner>

## All changes are for Comments and Notifications

If you’re not using Comments or Notifications, there are no breaking changes for
you! However, if you are using these, or intend to use them in future, keep
reading.

## Security

Users must see the notifications across multiple rooms when they open their
inbox. This is the first time we must expose an API not associated with a single
room. It forced us to introduce a minor breaking change in our authentication
mechanism. Using
[`useInboxNotifications`](/docs/api-reference/liveblocks-react#useInboxNotifications)
will call your authentication endpoint without a `room`.

### createClient with callback

If you use `createClient` with `authEndpoint`
[as a callback](/docs/api-reference/liveblocks-client#createClientCallback),
`room` is now optional.

```typescript
const client = createClient({
  authEndpoint: (room) => {
    // The `room` argument will be `undefined` if you're using `useInboxNotifications`
  },
});
```

## Comments resolver functions

Notifications needs to resolve users, as well as Comments, so we’ve lifted the
resolver functions from `createRoomContext` to `createClient`.

Before:

```tsx file="liveblocks.config.ts" highlight="7-12"
// ❌ Before - Liveblocks 1.9
const client = createClient({
  authEndpoint: "/api/liveblocks-auth",
});

const { RoomProvider } = createRoomContext({
  async resolveUsers({ userIds, roomId }) {
    // ...
  },
  async resolveMentionSuggestions({ text, roomId }) {
    // ...
  },
});
```

After:

```tsx file="liveblocks.config.ts" highlight="4-9"
// ✅ After - Liveblocks 1.10
const client = createClient({
  authEndpoint: "/api/auth/liveblocks-auth",
  async resolveUsers({ userIds }) {
    // ...
  },
  async resolveMentionSuggestions({ text }) {
    // ...
  },
});

const { RoomProvider } = createRoomContext();
```

Because the resolvers are no longer room-based, you’ll notice that `roomId` is
no longer in the parameters.

### Resolver types

Because the resolvers have moved to the `client`, the `ResolveUsersArgs` and
`ResolveMentionSuggestionsArgs` types are now exported from `@liveblocks/client`
instead of `@liveblocks/react`.

```tsx highlight="5,11"
// ❌ Before - Liveblocks 1.9
import {
  ResolveUsersArgs,
  ResolveMentionSuggestionsArgs,
} from "@liveblocks/react";

// ✅ After - Liveblocks 1.10
import {
  ResolveUsersArgs,
  ResolveMentionSuggestionsArgs,
} from "@liveblocks/client";
```

Relatedly,
[`stringifyCommentBody`](/docs/api-reference/liveblocks-node#stringify-comment-body)’s
`CommentBodyResolveUsersArgs` no longer exists, and you should use
`ResolveUsersArgs` instead.

```tsx highlight="5,11"
// ❌ Before - Liveblocks 1.9
import { CommentBodyResolveUsersArgs } from "@liveblocks/node";

// ✅ After - Liveblocks 1.10
import { ResolveUsersArgs } from "@liveblocks/node";
```

## Comments CSS variables

Some `elevation` and `tooltip`
[CSS variables](/docs/api-reference/liveblocks-react-ui#CSS-variables) have been
removed:

- `--lb-tooltip-background`
- `--lb-tooltip-foreground`
- `--lb-tooltip-foreground-contrast`
- `--lb-elevation-background`
- `--lb-elevation-foreground`
- `--lb-elevation-foreground-contrast`

Because these no longer exist, we recommend using the basic `--lb-*` variables
directly on `.lb-elevation` or `.lb-tooltip `.

Before:

```css
/* ❌ Before - Liveblocks 1.9 */
:root {
  --lb-tooltip-foreground: white;
  --lb-elevation-background: red;
}
```

After:

```css
/* ✅ After - Liveblocks 1.10 */
.lb-tooltip {
  --lb-foreground: white;
}

.lb-elevation {
  --lb-background: red;
}
```

## Comments overrides

Comments overrides allows you to replace a string or node in default components,
with another that you’ve specified. Some overrides have been changed.

- `SELF` is now `USER_SELF`.
- `UNKNOWN_USER` is now `USER_UNKNOWN`.
- `COMMENT_REACTION_REMAINING` no longer exists, and you should use the
  `LIST_REMAINING_USERS` instead.
- `COMMENT_REACTION_TOOLTIP` is now `COMMENT_REACTION_LIST`, and its arguments
  have been changed to `(list, emoji, count)`.

Before:

```tsx highlight="4-7"
// ❌ Before - Liveblocks 1.9
<CommentsConfig
  overrides={{
    SELF: "you",
    UNKNOWN_USER: "Anon",
    COMMENT_REACTION_REMAINING: (others) => `${others} people`,
    COMMENT_REACTION_TOOLTIP: (emoji, list) => (
      <>
        {list} reacted with {emoji}
      </>
    ),
  }}
/>
```

After:

```tsx highlight="4-7"
// ✅ After - Liveblocks 1.10
<CommentsConfig
  overrides={{
    USER_SELF: "you",
    USER_UNKNOWN: "Anon",
    LIST_REMAINING_USERS: (others) => `${others} people`,
    COMMENT_REACTION_LIST: (list, emoji, count) => (
      <>
        {list} reacted with {emoji}
      </>
    ),
  }}
/>
```

When upgrading, remember that overrides can be applied both globally with
`<CommentsConfig>`, and per component, e.g. `<Composer>`. Learn more in the
[overrides API reference](/docs/api-reference/liveblocks-react-ui#Overrides).

---
meta:
  title: "Upgrading to 1.2"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 1.2"
---

There are no breaking changes in this update, however we are introducing a new
authentication method. If you’re currently using `createClient()` with the
`authEndpoint` option, we recommend you read on.

Liveblocks 1.2 provides new ways to specify who can access certain rooms, and
with which permissions they can enter. Our new-style security tokens also bring
speed improvements, allowing clients to connect Liveblocks rooms even quicker,
as well as permitting authorization tokens that can be used for multiple rooms.

## Public key authentication changes [#public-auth-changes]

<Banner type="success" title="No changes required">
  If you’re currently using Liveblocks **public keys**, no changes are required.
</Banner>

With Liveblocks 1.2, entering rooms with your public key will be noticeably
quicker. If you’re currently using Liveblocks public keys, no changes are
required to your application.

## Private key authentication changes [#private-auth-changes]

<Banner type="warning" title="We recommend changing your auth back end">
  If you’re currently using Liveblocks **private keys** we **highly recommend**
  you upgrade your auth back end.
</Banner>

Upgrading your existing auth back end will opt you in to using our new-style
auth tokens, which offer the following benefits:

- Grant users permission to multiple rooms in one transaction, meaning fewer
  requests on your back end.
- Much quicker to join rooms, particularly any after the first.
- Unlock access to upcoming features, such as [Comments](/comments).

If you’re currently using Liveblocks private keys, no changes are strictly
necessary, but we do highly recommend you upgrade your application’s back end.

## How to upgrade? [#how]

You can upgrade to 1.2 by downloading the latest version of each Liveblocks
package you’re using, for example in a React app:

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

<Banner title="Update every Liveblocks package">

If you’re using any other Liveblocks packages make sure to update those too.

</Banner>

We’ll walk you through the necessary changes below, but first, if you currently
have a Liveblocks application in production, we recommend following a rollout
plan, to prevent any users running an old Liveblocks client, during the upgrade,
from having issues.

### Rollout plan

<Steps>
  <StepCompact>
    Keep your existing back end endpoint (e.g. `/api/auth`).
  </StepCompact>

<StepCompact>
  Create a new authentication endpoint for the upgrade (e.g.
  `/api/liveblocks-auth`).
</StepCompact>

<StepCompact>
  In your front end, point `createClient()`’s `authEndpoint` URL to the new
  endpoint.
</StepCompact>

<StepCompact>Done! You can deploy your application now.</StepCompact>

  <StepCompact lastStep>
    Later, when all your application’s clients have been upgraded to the latest version, you can safely remove the old endpoint.
  </StepCompact>
</Steps>

## Deciding which token to use [#deciding]

In Liveblocks 1.2 there are two new ways to authenticate with
`@liveblocks/node`.

- [Access tokens](#access-tokens) are recommend for most applications.
- [ID tokens](#id-tokens) are best if you’re using fine-grained permissions with
  our REST API.

Access tokens and ID tokens both allow for multiple room support, but have
different sources of truth. The old authentication method, single-room tokens
with `authorize`, is still supported but will eventually be deprecated.

## Access tokens [#access-tokens]

Access tokens are the new recommended way to authenticate, because they’re easy
to manage from your custom back end. They follow the analogy of a _hotel key
card_. Anyone that has a key card can enter any room that the card gives access
to. It’s easy to give out these key cards right from your back end.

### Upgrading to access tokens [#upgrade-to-access-tokens]

First, create a new endpoint in your back end, next to your existing `/api/auth`
endpoint. We recommend going with `/api/liveblocks-auth`.

Let’s implement it to issue tokens that would be equivalent to your current
single-room token based setup.

<Steps>

  <Step>
    <StepTitle>Create a client</StepTitle>
    <StepContent>
    Create a Node.js client that allows you to interact with our REST API.
      ```tsx
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx",
      });
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Start an auth session inside your endpoint</StepTitle>
    <StepContent>
    Every session should have a unique user ID, which is typically the ID of the user in your database.
      ```tsx highlight="10-13"
      import { Liveblocks } from "@liveblocks/node";

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

      export async function POST(request) {
        const user = { id: "olivier@example.com", info: { name: "Olivier" }};

        const session = liveblocks.prepareSession(
          user.id,
          { userInfo: user.info } // Optional
        );
      }
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Give the user access to the room</StepTitle>
    <StepContent>
    Give if the current user access to the room, with either `session.FULL_ACCESS`, or `session.READ_ACCESS`.
      ```tsx highlight="15,16"
      import { Liveblocks } from "@liveblocks/node";

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

      export async function POST(request) {
        const user = { id: "olivier@example.com", info: { name: "Olivier" }};

        const session = liveblocks.prepareSession(
          user.id,
          { userInfo: user.info } // Optional
        );

        const { room } = await request.json();
        session.allow(room, session.FULL_ACCESS);
      }
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Authorize the user and return the result</StepTitle>
    <StepContent>
    Give if the current user access to the room, with either `session.FULL_ACCESS`, or `session.READ_ACCESS`.
      ```tsx highlight="18,19"
      import { Liveblocks } from "@liveblocks/node";

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

      export async function POST(request) {
        const user = { id: "olivier@example.com", info: { name: "Olivier" }};

        const session = liveblocks.prepareSession(
          user.id,
          { userInfo: user.info } // Optional
        );

        const { room } = await request.json();
        session.allow(room, session.FULL_ACCESS);

        const { status, body } = await session.authorize();
        return new Response(body, { status });
      }
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Point to the new endpoint</StepTitle>
    <StepContent>
    In the front end of your app, update your `liveblocks.config.ts` file to connect to the new endpoint.
    ```ts file="liveblocks.config.ts" highlight="4"
      import { createClient } from "@liveblocks/client";

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

  </Step>

<Step>
  <StepTitle>You’re migrated!</StepTitle>
  <StepContent>
    You’ve successfully migrated, and now have a similar authentication set up
    as before! However, there are other new features we can take advantage of.
  </StepContent>
</Step>

  <Step lastStep>
    <StepTitle>Bonus: Issue access to multiple rooms</StepTitle>
    <StepContent>
    With Liveblocks 1.2, you can also issue access to multiple rooms, or even use a prefix-based wildcard in the room name, enabling any amount of rooms! You can learn more about this in our [access token](/docs/authentication/access-token#permissions) guide.
      ```tsx highlight="15-18"
      import { Liveblocks } from "@liveblocks/node";

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

      export async function POST(request) {
        const user = { id: "olivier@example.com", info: { name: "Olivier" }};

        const session = liveblocks.prepareSession(
          user.id,
          { userInfo: user.info } // Optional
        );

        const { room } = await request.json();
        session.allow(room, session.FULL_ACCESS);
        session.allow("my-room-*", session.READ_ACCESS);
        session.allow("my-other-room", session.READ_ACCESS);

        const { status, body } = await session.authorize();
        return new Response(body, { status });
      }
      ```
    </StepContent>

  </Step>

</Steps>

### Learn more about access tokens

You can find guides for your specific framework and learn more about permissions
in our [access tokens authentication guides](/docs/authentication/access-token).

Here’s a full working example of access tokens in a Next.js endpoint.

```ts file="app/api/liveblocks-auth/route.ts" isCollapsed isCollapsable
import { Liveblocks } from "@liveblocks/node";

// Create a client
const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST(request: Request) {
  const user = __getUserFromDB__(request);

  // Start an auth session inside your endpoint
  const session = liveblocks.prepareSession(
    user.id,
    { userInfo: user.metadata } // Optional
  );

  // Give the user access to the room
  const { room } = await request.json();
  if (room && __shouldUserHaveAccess__(user, room)) {
    // e.g. session.allow("my-room", ["room:write"])
    session.allow(room, session.FULL_ACCESS);
  }

  // Authorize the user and return the result
  const { status, body } = await session.authorize();
  return new Response(body, { status });
}
```

## ID tokens [#id-tokens]

Are you already using our REST API to assign fine-grained permissions to each
room, via [Create room](/docs/api-reference/rest-api-endpoints#post-rooms) or
[Update room](/docs/api-reference/rest-api-endpoints#post-rooms-roomId) APIs? If
so, ID tokens may work best for you.

ID tokens follow the analogy of a _membership card_. Anyone with that membership
card can try to enter a room, but your permissions will be checked at the door.
This approach to permissions is most powerful because it can be set up very
finely, but it comes at the cost of having to keep those permissions
programmatically in sync with Liveblocks. We recommend it for advanced use cases
only.

### Upgrading to ID tokens [#upgrade-to-id-tokens]

If you’ve already set up room permissions using our REST API, then this should
be easy!

First, let’s create a new endpoint in your back end, next to your existing
`/api/auth` endpoint. We recommend going with `/api/liveblocks-auth`.

All you have to do now is implement it as follows:

<Steps>

  <Step>
    <StepTitle>Create a client</StepTitle>
    <StepContent>
    Create a Node.js client that allows you to interact with our REST API.
      ```tsx
      import { Liveblocks } from "@liveblocks/node";

      const liveblocks = new Liveblocks({
        secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx",
      });
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Identify the user and return the result</StepTitle>
    <StepContent>
    Whichever `userId` (or `groupIds`) you pass will be used to check the permissions you configured in your Liveblocks account already.
      ```tsx highlight="10-15"
      import { Liveblocks } from "@liveblocks/node";

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

      export async function POST(request) {
        const user = { id: "olivier@example.com", info: { name: "Olivier" }};

        const { status, body } = await liveblocks.identifyUser({
          userId: user.id,
          groupIds, // Optional
        });

        return new Response(body, { status });
      }
      ```
    </StepContent>

  </Step>

  <Step>
    <StepTitle>Point to the new endpoint</StepTitle>
    <StepContent>
    In the front end of your app, update your `liveblocks.config.ts` file to connect to the new endpoint.
    ```ts file="liveblocks.config.ts" highlight="4"
      import { createClient } from "@liveblocks/client";

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

  </Step>

<Step>
  <StepTitle>You’re migrated!</StepTitle>
  <StepContent>You’ve successfully migrated to id tokens!</StepContent>
</Step>

  <Step lastStep>
    <StepTitle>Bonus: Use our REST API to handle permissions</StepTitle>
    <StepContent>
    ID tokens use permissions set with our [REST API](/docs/api-reference/rest-api-endpoints).
    For example, this is how you [create a room](/docs/api-reference/rest-api-endpoints#post-rooms),
    and give a user full access.

    ```ts highlight="6-8"
    fetch("https://api.liveblocks.io/v2/rooms", {
      method: "POST",
      body: JSON.stringify({
        id: "my-room-name",
        defaultAccesses: [],
        usersAccesses: {
          "olivier@example.com": ["room:write"]
        }
      }),
    });
    ```
    </StepContent>

  </Step>
</Steps>

### Learn more about ID tokens

You can find guides for your specific framework and learn more about permissions
in our [ID tokens authentication guides](/docs/authentication/id-token).

Here’s a full working example of ID tokens in a Next.js endpoint.

```ts file="app/api/liveblocks-auth/route.ts" isCollapsed isCollapsable
import { Liveblocks } from "@liveblocks/node";

// Create a client
const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

export async function POST(request: Request) {
  const user = __getUserFromDB__(request);

  // Identify the user and return the result
  const { status, body } = await liveblocks.identifyUser({
    userId: user.id,
    groupIds, // Optional
  });

  return new Response(body, { status });
}
```

If you have issues with these new patterns and need help, please let us know
[by email](mailto:support@liveblocks.io) or by joining our
[Discord community](/discord)! We’re here to help!

---
meta:
  title: "Upgrading to 1.5"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 1.5"
---

There’s a small breaking changes in this update, as we’re introducing a better
API to enter and leave rooms. If you’re calling `client.enter()` or
`client.leave()` in your application, or using our Zustand or Redux packages, we
recommend you read on.

## How to upgrade? [#how]

You can upgrade to 1.5 by downloading the latest version of each Liveblocks
package you’re using, for example in a React app:

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

<Banner title="Update every Liveblocks package">

If you’re using any other Liveblocks packages make sure to update those too.

</Banner>

## Recommended: new enter/leave API

Until now, the API to manually enter a Room using the client looked like the
following:

```tsx
// ❌ We recommend you stop using this API
const room = client.enter("my-room", options);

// Then later, when unmounting
client.leave("my-room");
```

These APIs will remain supported and unchanged, but starting with Liveblocks
1.5, there is a new preferred API, which we recommend you switch to:

```tsx
// ✅ Prefer this API instead
const { room, leave } = client.enterRoom("my-room", options);

// Then later, when unmounting
leave();
```

We’ve changed this API to return a new “leave” function every time a room
reference is requested. This allows sharing the same room connection with two or
more parts of your application, without those parts competing for control of the
room connection. The room connection will only be terminated after every `leave`
function has been called.

This enables more advanced use cases such as supporting multiple `RoomProvider`
instances for the same room ID, in different parts of your application, or using
our React package for one part of your application, while using a Zustand store
for another.

## Upgrading for Zustand users

If you’re using our Zustand package, there’s a breaking change—you no longer
need to pass the room ID to leave the room:

```tsx
const {
  liveblocks: { leaveRoom },
} = useStore();

// ❌ Before
leaveRoom("my-room-name");

// ✅ After
leaveRoom();
```

## Upgrading for Redux users

If you’re using our Redux package, there’s a similar breaking change—you no
longer need to pass the room ID to leave the room:

```tsx
const dispatch = useDispatch();

// ❌ Before
dispatch(actions.leaveRoom("my-room-name"));

// ✅ After
dispatch(actions.leaveRoom());
```

---
meta:
  title: "Upgrading to 1.9"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 1.9"
---

There’s a tiny breaking change related to Comments in this update. Each date
returned is now a `Date` object, whereas previously each was a `string`.

## How to upgrade? [#how]

You can upgrade to 1.9 by downloading the latest version of each Liveblocks
package you’re using, for example in a React app:

```bash
npm install @liveblocks/client@latest @liveblocks/react@latest @liveblocks/react-comments@latest @liveblocks/node@latest
```

<Banner title="Update every Liveblocks package">

If you’re using any other Liveblocks packages make sure to update those too.

</Banner>

## Dates are no longer strings

Each date returned from Comments is now a `Date` object, whereas previously each
was a `string`. An example of this is `createdAt`, a value attached to each
thread.

Before:

```tsx
// ❌ Before - Liveblocks 1.8
const { threads } = useThreads();

// "2023-12-15T14:15:22Z"
console.log(threads[0].createdAt);
```

After:

```tsx
// ✅ After - Liveblocks 1.9
const { threads } = useThreads();

// Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>
console.log(threads[0].createdAt);
```

Every Comments-related date has been updated, such as `createdAt`, `updatedAt`
properties in threads, and `editedAt`, `deletedAt` in comments, so take care to
check every date instance you’re using. It’s likely that no changes are required
after this update.

---
meta:
  title: "Upgrading to 2.0"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 2.0"
---

Liveblocks 2.0 is our second major release, and focuses on removing any rough
edges and smoothening the developer experience. It makes Liveblocks simpler to
set up for new users as well as for existing users. Our long term goal is to
empower you to write and evolve your apps in the best way possible, and at
enterprise scale.

<Banner title="Just want to upgrade?">

Skip ahead to the [how to upgrade](#how-to-upgrade) section.

</Banner>

## Rationale

Liveblocks has always embraced the benefits of static typing with TypeScript.
Roughly two years ago, we published 0.17, which
[introduced](/blog/whats-new-in-v0-17) the pattern of calling
`createRoomContext()` and returning a “bundle” of type-safe hooks that you could
then use in your application. This pattern worked nicely because all the hooks
were bound to your custom type definitions for `Presence`, `Storage`, etc. in
one single place, and every Liveblocks API would deeply know about it. No need
for manual type annotations anywhere else in your codebase.

### Downsides to this approach

There were, however, a couple of downsides to this approach that have always
kept itching:

- Importing hooks from a local config file was a little bit awkward. It wasn’t
  an all too familiar pattern for most users.
- Re-exporting the list of hooks took maintenance. If we added a new hook, you’d
  have to also re-export it manually.
- Due to how TypeScript works, the type params you’d provide to
  `createRoomContext()` had a fixed ordering to them. If you wanted to specify
  only `Presence` and `RoomEvent`, you’d have to “skip” some type params, by
  doing `createRoomContext<Presence, {}, {}, RoomEvent>()` or similar.

### Introducing Liveblocks 2.0

With 2.0, this all becomes a lot simpler. We’ve done a lot of internal
refactoring to make this possible. We’ll dive in soon, but here is a sneak peek:

```tsx showLineNumbers={false} file="liveblocks.config.ts"
// ❌ This pattern is no longer recommended unless you need multiple room types
export const {
  suspense: { RoomProvider, useRoom, useThread },
} = createRoomContext<
  MyPresence,
  MyStorage,
  MyUserMeta,
  MyRoomEvent,
  MyThreadMetadata
>(client);

// ✅ After
declare global {
  // These custom types are all optional, just define the ones you want/need
  interface Liveblocks {
    Presence: MyPresence;
    Storage: MyStorage;
    UserMeta: MyUserMeta;
    RoomEvent: MyRoomEvent;
    ThreadMetadata: MyThreadMetadata;
  }
}
```

```tsx showLineNumbers={false} file="Component.tsx"
// ❌ Before
import { RoomProvider, useRoom, useThreads } from "./liveblocks.config.ts";

// ✅ After
import { RoomProvider, useRoom, useThreads } from "@liveblocks/react/suspense";
// or
import { RoomProvider, useRoom, useThreads } from "@liveblocks/react";
```

We’ll first go over all the breaking changes, then we’ll show you how to
simplify your codebase.

## How to upgrade [#how-to-upgrade]

First of all, let’s upgrade all Liveblocks dependencies to their latest
versions. The easiest way to do that is to run the following command:

```bash
npx create-liveblocks-app@latest --upgrade
```

There are also some **breaking changes** in this update. **Most users will not
run into any of these**, but there is a chance that some of these will affect
your situation. Making the necessary code adjustments should however be easy. In
many cases, we provide a codemod that makes the actual change for you, so you
don’t have to do so manually.

### Breaking change 1: renamed package [#bc1]

<Banner>

This breaking change only affects users of `@liveblocks/react-comments`.

</Banner>

We’ve renamed our package `@liveblocks/react-comments` to
`@liveblocks/react-ui`, because our library of pre-built UI components now
contains more than just Comments-related components. Please adjust your imports.

Run the following **codemod** or manually make the changes:

```bash
npx @liveblocks/codemod@latest react-comments-to-react-ui
```

This will change your imports like this:

```tsx showLineNumbers={false}
// ❌ Before
import { Thread } from "@liveblocks/react-comments";

// ✅ After
import { Thread } from "@liveblocks/react-ui";
```

And also:

```tsx showLineNumbers={false}
// ❌ Before
<CommentsConfig />

// ✅ After
<LiveblocksUIConfig />
```

### Breaking change 2: renamed exports in our Node package [#bc2]

<Banner>

This breaking change only affects users of `@liveblocks/node`.

</Banner>

To avoid confusion with the newly introduced custom
[`RoomInfo`](#New-custom-type-RoomInfo) type, we’ve renamed the `RoomInfo` type
in our `@liveblocks/node` package.

Run the following **codemod** or manually make the changes:

```bash
npx @liveblocks/codemod@latest room-info-to-room-data
```

This will change:

```tsx showLineNumbers={false}
// ❌ Before
import { RoomInfo } from "@liveblocks/node";
const rooms: RoomInfo[] = [];

// ✅ After
import { RoomData } from "@liveblocks/node";
const rooms: RoomData[] = [];
```

### Breaking change 3: client methods from our Node package no longer take type params [#bc3]

<Banner>

This breaking change only affects users of `@liveblocks/node`.

</Banner>

In `@liveblocks/node`, none of the client methods, like
`liveblocks.getThread()`, take type params any longer.

Make the following changes:

```tsx showLineNumbers={false}
// ❌ Before
await liveblocks.createThread<MyThreadMetadata>();
await liveblocks.editThreadMetadata<MyThreadMetadata>();
await liveblocks.getThreads<MyThreadMetadata>();
await liveblocks.getThread<MyThreadMetadata>();
//                         ^^^^^^^^^^^^^^^^ No longer possible

// ✅ After
await liveblocks.createThread();
await liveblocks.editThreadMetadata();
await liveblocks.getThreads();
await liveblocks.getThread();

// In liveblocks.config.ts
declare global {
  interface Liveblocks {
    ThreadMetadata: MyThreadMetadata;
  }
}
```

You should use global type augmentation instead. Please see
[“Simplifying your Liveblocks application”](#simplifying) below for a lot more
detail on this transition.

### Breaking change 4: changed default export to named export in Yjs package [#bc4]

<Banner>

This breaking change only affects users of `@liveblocks/yjs`.

</Banner>

To make discoverability and refactorings easier, and to avoid confusion with the
newly introduced [`LiveblocksProvider`][] in our React package, we’re no longer
using default exports, but named exports only.

Run the following **codemod** or manually make the changes:

```bash
npx @liveblocks/codemod@latest remove-yjs-default-export
```

This will change your import (and its usage) like this:

```tsx showLineNumbers={false}
// ❌ Before
import LiveblocksProvider from "@liveblocks/yjs";
const yDoc = new Y.Doc();
const yProvider = new LiveblocksProvider(room, yDoc);
```

```tsx showLineNumbers={false}
// ✅ After
import { LiveblocksYjsProvider } from "@liveblocks/yjs";
const yDoc = new Y.Doc();
const yProvider = new LiveblocksYjsProvider(room, yDoc);
```

### Breaking change 5: minor LiveList constructor change [#bc5]

<Banner>

This breaking change only affects users of Storage.

</Banner>

The `LiveList()` constructor’s argument is no longer optional, because it causes
unneeded but confusing type inference issues.

Run the following **codemod** or manually make the changes:

```bash
npx @liveblocks/codemod@latest live-list-constructor
```

This will add an array to empty `LiveList` constructors:

```tsx showLineNumbers={false}
// ❌ Before
const mylist = new LiveList();

// ✅ After
const mylist = new LiveList([]);
```

### Breaking change 6: new webhook event types [#bc6]

<Banner>

This breaking change only affects users of webhooks.

</Banner>

The webhook event `NotificationEvent`’s type can represent multiple kinds of
notifications: `"thread"`, `"textMention"`, and custom ones (e.g.
`"$myNotification"`).

If you were using properties only available on the `"thread"` kind (e.g.
`threadId`), you will need to first check for the kind of notification before
accessing them.

```ts showLineNumbers={false}
// ❌ Before
const threadId = event.data.threadId;

// ✅ After
if (event.data.kind === "thread") {
  const threadId = event.data.threadId;
}
```

### Breaking change 7: removed deprecated APIs [#bc7]

All of the following APIs have been removed in 2.0, as they were deprecated
multiple versions ago.

**Affecting `@liveblocks/client`:**

- `Client.enter()` has been replaced by `Client.enterRoom()`
- `Client.leave()` has been replaced by `Client.enterRoom()`, which returns a
  `leave` function
- Client option `fetchPolyfill`, `WebSocketPolyfill`, are replaced by
  `polyfills: { fetch, WebSocket }`
- Legacy option `shouldInitiallyConnect` is now renamed to `autoConnect`
- Legacy connection status APIs, e.g. `room.getConnectionState()` and
  `.subscribe("connection")`. You can use `.getStatus()` or
  `.subscribe("status")` instead.
- `user.isReadOnly` field is replaced by `!user.canWrite` (note the negation
  here)
- The `Others<P, U>` type. Please change to `readonly User<P, U>[]`.

**Affecting `@liveblocks/react`:**

- The `useMap`, `useList`, and `useObject` hooks. These have been deprecated
  since the release of 0.18 (more than two years ago). Please see
  [the 0.18 upgrade guide](/docs/platform/upgrading/0.18#Step-5a:-Replace-reads-with-useStorage)
  for tips on how to rewrite these hooks to `useStorage`.
- The second argument `options` to `createRoomContext(client, options)` has been
  removed. These options have been moved to the client.

**Affecting `@liveblocks/node`:**

- Remove legacy `authorize` method from `@liveblocks/node`. Please refer to
  [the 1.2 upgrade guide](/docs/platform/upgrading/1.2#how) to learn how to
  upgrade your auth endpoint.

**Affecting `@liveblocks/redux` and `@liveblocks/zustand`:**

- Legacy aliased exports in Zustand/Redux packages are removed

## Simplifying your Liveblocks application (optional) [#simplifying]

<Banner title="Optional, but recommended">

This section is optional, but highly recommended. You can do it now, or come
back to do it later.

</Banner>

If you have dealt with the breaking changes above, or concluded they they don’t
apply to your situation, we can go a step further and really simplify your
Liveblocks setup.

<Banner type="warning" title="Do you have multiple room types?">

If your Liveblocks application has multiple room types, and therefore you have
multiple Liveblocks config files—one for each room type—then you cannot switch
to a single global type definition. In that advanced case, the following
simplifications do not apply to you. We will keep supporting the
`createRoomContext` API for these advanced use cases, but no longer recommend it
for the more common case of having just a single room type.

</Banner>

### Step 1: Use the new global Liveblocks custom types [#step1]

Go to your `liveblocks.config.ts`, find your `createRoomContext()` call. Now
decide which codemod to run.

**Option 1: You are using Suspense hooks.**
(`const { suspense: { useRoom, ... } }`)

```bash
npx @liveblocks/codemod@latest remove-liveblocks-config-contexts --suspense
```

**Option 2: You are using classic hooks.** (`const { useRoom, ... }`)

```bash
npx @liveblocks/codemod@latest remove-liveblocks-config-contexts
```

Running either of these will make the following changes.

```tsx showLineNumbers={false}
// ❌ Before
export const {
  ...
} = createRoomContext<MyPresence, MyStorage, MyUserMeta, MyRoomEvent, MyThreadMetadata>(client);
```

```tsx showLineNumbers={false}
// ✅ After
declare global {
  interface Liveblocks {
    Presence: MyPresence;
    Storage: MyStorage;
    UserMeta: MyUserMeta;
    RoomEvent: MyRoomEvent;
    ThreadMetadata: MyThreadMetadata;
  }
}
```

Secondly, it will change all imports in your code base to import the hooks
directly instead:

```tsx showLineNumbers={false}
// ❌ Before
import { RoomProvider, useRoom, ... } from "./liveblocks.config.ts";
```

```tsx showLineNumbers={false}
// ✅ After
import { RoomProvider, useRoom, ... } from "@liveblocks/react/suspense";  // Option 1
import { RoomProvider, useRoom, ... } from "@liveblocks/react";           // Option 2
```

### Step 2: Set up a LiveblocksProvider [#step2]

At this point, there should not be any new TypeScript issues. However, running
the code will not yet work. This is because previously the hooks were bound to
the `client` instance by passing it to the `createRoomContext()` factory, which
we now removed. When using the global types, we’ll have to provide the
Liveblocks client otherwise.

The way to do it is to use a pretty standard React provider. Make the following
change:

```tsx showLineNumbers={false} file="liveblocks.config.ts"
// ❌ Before, you no longer have to use createClient()
import { createClient } from "@liveblocks/client";

const client = createClient({
  authEndpoint: "/api/liveblocks-auth",
  throttle: 16,
  /* etc */
);
```

```tsx showLineNumbers={false} file="layout.tsx"
// ✅ After
"use client";

import { LiveblocksProvider } from "@liveblocks/react";

export function Layout({ children }) {
  return (
    // Move options here, client will be created for you
    <LiveblocksProvider
      authEndpoint="/api/liveblocks-auth"
      throttle={16}
      /* etc */
    >
      {children}
    </LiveblocksProvider>
  );
}
```

<Banner type="info" title="Where to put this?">

We recommend placing the `LiveblocksProvider` as high up your component tree as
you can, preferably in your `_app.tsx` or `layout.tsx` (or equivalent for your
React framework of choice). Unfortunately, we cannot provide a codemod for this
step, because we cannot mechanically decide where exactly to inject the
Liveblocks provider in your application.

</Banner>

If you were exporting the `client` instance before and have components that
directly accessed it before, you can now obtain a reference to the `client`
instance that the `LiveblocksProvider` creates for you using the `useClient`
hook:

```tsx showLineNumbers={false}
// ❌ Before
import { client } from "./liveblocks.config";

function MyComponent() {
  doSomethingWith(client);
}
```

```tsx showLineNumbers={false}
// ✅ After
import { useClient } from "@liveblocks/react"; // or
import { useClient } from "@liveblocks/react/suspense";

function MyComponent() {
  const client = useClient();
  doSomethingWith(client);
}
```

### Step 3: Optional cleanup of type params [#step3]

If you also exported your `Presence`, `Storage`, etc types from
`liveblocks.config.ts` before, you no longer have to. The main reason to export
these before was to use them in helper functions that used some of the
Liveblocks types, like `User`, or `Room`.

```bash
npx @liveblocks/codemod@latest remove-unneeded-type-params
```

For example:

```tsx showLineNumbers={false}
import type { Room, User } from "@liveblocks/client";
import type { MyPresence, MyStorage } from "./liveblocks.config";

// ❌ Before
function isAdult(user: User<MyPresence, MyUserMeta>) {
  //                        ^^^^^^^^^^^^^^^^^^^^^^
  return user.info.age >= 18;
  //               ^^^ Coming from MyUserMeta
}

// ❌ Before
function doSomethingWithRoom(
  room: Room<MyPresence, MyStorage, MyUserMeta, never>
  //         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
) {
  /* ... */
}
```

This is no longer needed. You can simply remove them. TypeScript will still know
about your custom `age` property on `user.info`.

```tsx showLineNumbers={false}
import type { Room, User } from "@liveblocks/client";

// ✅ After
function isAdult(user: User) {
  return user.info.age >= 18;
  //               ^^^ Still coming from configured UserMeta custom type
}

// ✅ After
function doSomethingWithRoom(room: Room) {
  /* ... */
}
```

## Improvements

Furthermore, the following miscellaneous quality-of-life improvements have been
made that are non-breaking changes.

### `ClientSideSuspense` no longer needs a function

Previously, the `ClientSideSuspense` helper needed a function as its `children`
prop, but it no longer has to.

```bash
npx @liveblocks/codemod@latest simplify-client-side-suspense-children
```

This will change:

```tsx showLineNumbers={false}
// ❌ Before
<ClientSideSuspense fallback={<Loading />}>
  {() => <MyApp />}
</ClientSideSuspense>
```

```tsx showLineNumbers={false}
// ✅ After
<ClientSideSuspense fallback={<Loading />}>
  <MyApp />
</ClientSideSuspense>
```

### Improved `InboxNotification` props types

When passing custom components to the `kinds` prop of [`InboxNotification`][],
you could use types like `InboxNotificationThreadProps` for the props. But this
wasn’t always true for all notification kinds, so now you can use types named
`InboxNotificationThreadKindProps` for your components, while
`InboxNotificationThreadProps` describes the props of our own
`InboxNotification.Thread`.

```tsx showLineNumbers={false}
// ❌ Before
function MyThreadNotification(props: InboxNotificationThreadProps) {
  return <InboxNotification.Thread {...props} />;
}

<InboxNotification kinds={{ thread: MyThreadNotification }} />;
```

```tsx showLineNumbers={false}
// ✅ After
function MyThreadNotification(props: InboxNotificationThreadKindProps) {
  //                                                        ^^^^
  return <InboxNotification.Thread {...props} />;
}

<InboxNotification kinds={{ thread: MyThreadNotification }} />;
```

### New custom type `RoomInfo`

By using the [`resolveRoomsInfo`][] callback from [`createClient`][] or the new
[`LiveblocksProvider`][], you can attach arbitrary room data to a room, which
you can retrieve with the [`useRoomInfo`][] hook.

Both of these APIs will now respect the type you provide via:

```tsx showLineNumbers={false}
declare global {
  interface Liveblocks {
    RoomInfo: {
      /* your custom type definition here */
    };
  }
}
```

### New custom type `ActivitiesData`

By providing a custom `ActivitiesData` type, you can improve how your custom
notifications and their activities’ data are typed.

```tsx showLineNumbers={false}
declare global {
  interface Liveblocks {
    // Custom activities data for custom notification kinds
    ActivitiesData: {
      // Example, a custom $alert kind
      $alert: {
        title: string;
        message: string;
      };
    };

    // Other kinds
    // ...
  }
}
```

That’s it!

## Questions? Please ask!

If you have any trouble with these new patterns, run into a bug with one of the
codemods, or otherwise need help, please let us know
[by email](mailto:support@liveblocks.io) or by joining our
[Discord community](/discord)! We’re here to help!

[`createClient`]: /docs/api-reference/liveblocks-client#createClient
[`LiveblocksProvider`]: /docs/api-reference/liveblocks-react#LiveblocksProvider
[`InboxNotification`]: /docs/api-reference/liveblocks-react-ui#InboxNotification
[`resolveRoomsInfo`]:
  /docs/api-reference/liveblocks-client#createClientResolveRoomsInfo
[`useRoomInfo`]: /docs/api-reference/liveblocks-react#useRoomInfo

---
meta:
  title: "Upgrading to 2.15"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 2.15"
---

This version of Liveblocks drops support for React versions below 18.

## Does this affect you? [#does-this-affect-you]

If you are **already on React 18 or above**, you do not have to do anything.
Just enjoy the smaller bundle size and performance improvements!

If **you are on React 16 or 17**, you should upgrade React before upgrading
Liveblocks to 2.15. See the [instructions below](#how-to-upgrade).

If **you cannot upgrade to React 18 yet** for whatever reason, you can still
keep using Liveblocks 2.14 which is the last version to support React 17 and
below.

## How to upgrade? [#how-to-upgrade]

To upgrade Liveblocks to 2.15, first upgrade React to 18 by following the
[How to Upgrade to React 18](https://react.dev/blog/2022/03/08/react-18-upgrade-guide).

Then, upgrade all Liveblocks packages by running:

```bash
npx create-liveblocks-app@latest --upgrade
```

You can now safely remove this line from your codebase:

```diff
-import { unstable_batchedUpdates } from "react-dom";
 //                                      ^^^^^^^^^^^ ...or "react-native"!

 <RoomProvider
   id="my-room"
   initialPresence={/* ... */}
   initialStorage={/* ... */}
-  unstable_batchedUpdates={unstable_batchedUpdates}
 >
   <App />
 </RoomProvider>
```

That’s it!

---
meta:
  title: "Upgrading to 2.16"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 2.16"
---

We’ve made React changes, affecting how our error listener hook works, and how
undefined metadata is filtered in Comments.

## How to upgrade

Upgrade to 2.16 by downloading the latest version of each Liveblocks package
you’re using. The easiest way to do this is to run the following command:

```bash
npx create-liveblocks-app@latest --upgrade
```

## Does this affect you? [#does-this-affect-you]

**If you’re using the [`useErrorListener`][] hook**, please read about
[a behavior change](#change-1).

**If you’re using Comments in React**, please read about
[the change to metadata filtering](#change-2).

Otherwise, no changes will affect you.

## New errors can appear in `useErrorListener` [#change-1]

From 2.16, [`useErrorListener`][] will notify you about new errors. Previously,
only room connection errors were previously reported, possible if you were using
Presence, Storage, or Yjs. After upgrading, you can also receive a number of
errors from Comments and Notifications.

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

useErrorListener((error) => {
  switch (error.context.type) {
    // +++
    case "CREATE_THREAD_ERROR":
      const { roomId, threadId, commentId, body, metadata } = error.context;
      break;
    // +++

    // +++
    case "MARK_INBOX_NOTIFICATION_AS_READ_ERROR":
      const { inboxNotificationId, roomId } = error.context;
      break;
    // +++

    // Many other new errors
    // ...
  }
});
```

For a full list of possible errors, see the [`useErrorListener`][]
documentation.

We have also decoupled [`useErrorListener`][] from the current room. Previously
it required to be nested under a
[`RoomProvider`](/docs/api-reference/liveblocks-react#RoomProvider). Now, you
can use it anywhere under
[`LiveblocksProvider`](/docs/api-reference/liveblocks-react#LiveblocksProvider),
and it will notify you about errors from all rooms.

### To upgrade [#upgrade-change-1]

No changes are necessary to upgrade, however **if you would like to keep the old
behavior** and only show room connection errors, you can filter out all new
errors with an early return:

```ts
import { useErrorListener, useRoom } from "@liveblocks/react";

function App() {
  // +++
  const room = useRoom();
  // +++

  useErrorListener((error) => {
    // +++
    if (
      error.context.roomId !== room.id &&
      error.context.type !== "ROOM_CONNECTION_ERROR"
    ) {
      return;
    }
    // +++

    // Your previous logic
  });
}
```

## Ability to filter threads by absence of metadata [#change-2]

From 2.16, the [`useThreads`][] (and `useUserThreads_experimental`) hooks
supports filtering by _absence_ of a metadata field using `null`.

```ts
// ✅ Will now return "important" threads without a `color` field
const threads = useThreads({
  query: {
    metadata: {
      // +++
      color: null,
      // +++
      label: "important",
    },
  },
});
```

Previously this was not supported, but due to a bug, using an explicit
`undefined` would already allow you to do this. However, threads weren’t
filtered correctly in our back end causing over-fetches.

### To upgrade [#upgrade-change-2]

If you relied on this bug, change `undefined` to `null` in any metadata
properties. **This will keep behavior the same**—if you don’t make this change,
then your previous filtering will no longer work.

```ts title="Before"
const threads = useThreads({
  query: {
    metadata: {
      // ❌ Before - Will have no effect in 2.16
      // +++
      color: undefined,
      // +++
      label: "important",
    },
  },
});
```

```ts title="After"
const threads = useThreads({
  query: {
    metadata: {
      // ✅ After - Will now find threads without a `color`
      // +++
      color: null,
      // +++
      label: "important",
    },
  },
});
```

That’s it for 2.16!

[`useerrorlistener`]: /docs/api-reference/liveblocks-react#useErrorListener
[`usethreads`]: /docs/api-reference/liveblocks-react#useThreads

---
meta:
  title: "Upgrading to 2.2"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 2.2"
---

<Banner title="Upgrade to 2.0 required">

You must upgrade to 2.0 first, please follow the
[2.0 upgrade guide](/docs/platform/upgrading/2.0).

</Banner>

We are making `resolved` a first-class citizen property on
[threads](/docs/ready-made-features/comments/concepts#Threads). You don’t have
to use a thread’s metadata to set a thread as resolved anymore.

## How to upgrade? [#how]

You can upgrade to 2.2 by downloading the latest version of each Liveblocks
package you’re using. The easiest way to do that is to run the following
command:

```bash
npx create-liveblocks-app@latest --upgrade
```

## All changes are for Comments

If you are not using Comments or are not using the `resolved` metadata property,
there are no breaking changes for you! However, if you are and have logic based
on the webhook event `threadMetadataUpdated`, keep reading.

### Webhook events

When you upgrade to 2.2, the default
[`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component will update
the thread’s first-class citizen property instead of the metadata. When a thread
is marked as resolved or unresolved, we will send the events
[`threadMarkedAsResolved`](/docs/platform/webhooks#ThreadMarkedAsResolvedEvent)
and
[`threadMarkedAsUnresolved`](/docs/platform/webhooks#ThreadMarkedAsUnresolvedEvent)
instead of
[`threadMetadataUpdated`](/docs/platform/webhooks#ThreadMarkedAsUnresolvedEvent).

Before upgrading to 2.2, you should update your webhook endpoint to process
those new events accordingly.

### REST API endpoints

If you use the REST endpoint
[`Edit thread metadata`](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-metadata)
to update the metadata `resolved`, you should instead use the endpoints
[`Mark thread as resolved`](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-resolved)
and
[`Mark thread as unresolved`](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-mark-as-unresolved).

Note that updating the `resolved` metadata property automatically updates the
first-class citizen one.

---
meta:
  title: "Upgrading to 2.24"
  parentTitle: "Upgrading"
  description: "Guide to upgrade to Liveblocks version 2.24"
---

We’ve renamed some of the concepts around notifications and notification
settings to improve clarity.

## How to upgrade

Upgrade to 2.24 by downloading the latest version of each Liveblocks package
you’re using. The easiest way to do this is to run the following command:

```bash
npx create-liveblocks-app@latest --upgrade
```

## Does this affect you? [#does-this-affect-you]

If you are using notification settings in any way—with [`@liveblocks/client`][],
[`@liveblocks/react`][], [`@liveblocks/node`][], or
[the REST API](/docs/api-reference/rest-api-endpoints)—continue reading to see
the changes. The naming changes are **backwards compatible**.

Otherwise, no changes will affect you.

## Naming changes

### Rationale

Historically, we used the term “notification settings” to refer to
**room-level** settings that control the
[inbox notifications](/docs/ready-made-features/notifications/concepts#Inbox-Notifications)
received by a user (e.g. setting `"threads"` to `"all"` would make the user
receive inbox notifications for any threads activity in the room, not just the
ones where they participate).

With 2.18, we introduced the concept of “**user** notification settings”, which
are **project-level** settings that control the notifications (
[`"notification"` webhook events](/docs/platform/webhooks#NotificationEvent),
not "inbox notifications") received by a user (e.g. setting
`email.$myCustomNotification` to `false` would disable `"notification"` webhook
events for the `$myCustomNotification` kind and the `email` channel).

To improve clarity, we’re renaming these concepts:

- “room **notification** settings” → “room **subscription** settings”: they
  control which things a user is subscribed to in a room
- “**user** notification settings” → “notification settings”: they control which
  notifications a user receives

### What changed

All methods, hooks, types, and REST API endpoints that were using these terms
have been renamed to use the new naming, see the examples below.

We provide a codemod to automatically update your codebase to the new naming.

```bash
npx @liveblocks/codemod@latest rename-notification-settings
```

#### `@liveblocks/react`

```tsx
// ❌ Before
import {
  // +++
  useRoomNotificationSettings,
  useUpdateRoomNotificationSettings,
  // +++
} from "@liveblocks/react";

// ✅ After
import {
  // +++
  useRoomSubscriptionSettings,
  useUpdateRoomSubscriptionSettings,
  // +++
} from "@liveblocks/react";
```

```tsx
// ❌ Before
useErrorListener((error) =>
  // +++
  if (error.context.type === "UPDATE_NOTIFICATION_SETTINGS_ERROR") {
    // +++
    /* ... */
  }
);

// ✅ After
useErrorListener((error) =>
  // +++
  if (error.context.type === "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR") {
    // +++
    /* ... */
  }
);
```

#### `@liveblocks/client`

```tsx
// ❌ Before
import type { RoomNotificationSettings } from "@liveblocks/client";

// ✅ After
import type { RoomSubscriptionSettings } from "@liveblocks/client";
```

```tsx
// ❌ Before
const { room, leave } = client.enterRoom("my-room-id");

// +++
room.getNotificationSettings();
room.updateNotificationSettings(/* ... */);
// +++

// ✅ After
const { room, leave } = client.enterRoom("my-room-id");

// +++
room.getSubscriptionSettings();
room.updateSubscriptionSettings(/* ... */);
// +++
```

```tsx
// ❌ Before
import type { UserNotificationSettings } from "@liveblocks/client";

// ✅ After
import type { NotificationSettings } from "@liveblocks/client";
```

#### `@liveblocks/node`

```tsx
// ❌ Before
const liveblocks = new Liveblocks({
  secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx",
});

// +++
await liveblocks.getRoomNotificationSettings(/* ... */);
await liveblocks.updateRoomNotificationSettings(/* ... */);
await liveblocks.deleteRoomNotificationSettings(/* ... */);
// +++

// ✅ After
const liveblocks = new Liveblocks({
  secret: "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx",
});

// +++
await liveblocks.getRoomSubscriptionSettings(/* ... */);
await liveblocks.updateRoomSubscriptionSettings(/* ... */);
await liveblocks.deleteRoomSubscriptionSettings(/* ... */);
// +++
```

#### REST API

```shell