Add live content to your application

Learn to use the Live Content API with Next.js or your own integration for real-time content updates in your app.

The Live Content API lets you deliver live content experiences without the complexity and infrastructure requirements traditionally found in real-time apps.

The next-sanity library wraps the Live Content API for Next.js apps. The JavaScript client offers helper utilities to get you started, but you'll need to build additional functionality.

This guide shows two ways to add live content to an application: with next-sanity in a Next.js app, and with the JavaScript client in any other framework.

Add live content with next-sanity

Enable live content with only a few lines of code with next-sanity.

Next.js + Sanity + Visual Editing

If you plan to set up Next.js, Sanity, Visual Editing, and the Live Content API, see the Next.js Visual Editing guide for a complete implementation.

Prerequisites

  • A new or existing Sanity project.
  • Add your frontend or deployment target's origin to the project's CORS origins. This is found in the project's API section at sanity.io/manage.
  • A Next.js application built with the app router architecture. The Live Content features in next-sanity do not support apps built with the pages router.
  • This guide assumes next-sanity v13 or later, which requires Next.js 16, React 19.2 or later, and @sanity/client 7.26.2 or later, including 8.x.
  • The code in this section assumes cacheComponents is off in next.config.ts. New apps from create-next-app 16.4 and later turn it on, and sanityFetch then has to run inside a use cache boundary. Either set cacheComponents: false while you follow this guide, or follow Sanity Live with Next.js Cache Components instead.

Install and configure the client

You can install, set up, and configure Sanity in your existing Next.js project with init:

npx sanity@latest init

Alternatively, install the package or update it to the latest version:

npm install next-sanity@latest

Next, confirm that you have an existing Sanity client configured:

import { createClient } from "next-sanity";

import { dataset, projectId } from "../env";

export const client = createClient({
  projectId,
  dataset,
  apiVersion: "2026-03-01",
  useCdn: true
});

Create the live utilities

Create a live utility file and configure the sanityFetch helper and SanityLive component by passing in your local Sanity client and a token. defineLive requires a browser and server token to fetch draft content when using Draft Mode. If you aren't using Visual Editing or draft previews, set serverToken: false and browserToken: false to opt out and silence the development warnings:

import { defineLive } from "next-sanity/live";
// import your local configured client
import { client } from "@/sanity/lib/client";

// set your viewer token
const token = process.env.SANITY_API_READ_TOKEN
if (!token) {
  throw new Error("Missing SANITY_API_READ_TOKEN")
}

// export the sanityFetch helper and the SanityLive component
export const { sanityFetch, SanityLive } = defineLive({
  client,
  serverToken: token,
  browserToken: token,
})

Tokens

Fetch your queries

Whenever you need to query data in your Sanity dataset, import the sanityFetch helper and call it as you would any Sanity client by passing in a GROQ query and any query parameters:

import { sanityFetch } from "@/sanity/lib/live"
import { POST_QUERY } from "./queries"

const {data: post} = await sanityFetch({query: POST_QUERY, params: {}})

In this example, the data response is destructured to post and sanityFetch receives a GROQ query and an optional params object.

Enable the SanityLive component

The final step to enable the Live Content API is adding the SanityLive React component. It listens for changes in your data and works with your sanityFetch queries to efficiently update content. Include it in your application so it renders on any page that needs live content.

Embedded studios

In this example, it lives just before the closing body tag in the RootLayout component:

import { SanityLive } from "@/sanity/lib/live"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <SanityLive />
      </body>
    </html>
  )
}

Make updates instant in next-sanity v13

Next steps

Create your own integration

If there isn't an official library for your framework that enables live content, you need to create your own integration to use the Live Content API. The Live Content API Examples repository on GitHub collects example projects and is a good starting point for custom implementations.

The minimal example in this section uses the Sanity JavaScript client.

Prerequisites

  • API version v2021-03-25 or later. Older versions omit syncTags from query responses and throw The live events API requires API version 2021-03-25 or later.
  • The real dataset name. The Live Content API does not support dataset aliases.
  • A new or existing Sanity project.
  • Add your frontend or deployment target's origin to the project's CORS origins. This is found in the project's API section at sanity.io/manage.

Install and configure the client

First, install the latest version of the client:

npm install @sanity/client@latest

Configure your @sanity/client with your project settings and the latest API version:

import { createClient } from "@sanity/client"

export const client = createClient({
  projectId: "",
  dataset: "",
  apiVersion: "2026-03-01",
  useCdn: true
})

How it works

Here's a high-level overview of how the Live Content API works:

  • Every response from Content Lake includes sync tags. Your application stores the tags for the content it needs to keep up to date in real time.
  • It subscribes to a stream of live updates with the client.live.events() method, which returns an Observable that emits an event whenever content in the dataset changes.
  • When an event arrives, it checks whether any of the event tags match the stored sync tags.
  • If there's a match, it refetches the content, passing the event ID as the lastLiveEventId argument to client.fetch so the CDN returns the latest version of the content instead of stale data.

Minimal example

Here is a minimal example running in the console. It keeps a single, predefined document in sync using sync tags:

import { createClient } from "@sanity/client"

// Create the client instance
const client = createClient({
  projectId: "",
  dataset: "",
  apiVersion: "2026-03-01",
  useCdn: true
})

const query = '*[_type == "post" && slug.current == $slug][0]'
const slug = "were-doing-it-live"

let syncTags = []

function render(lastLiveEventId?: string) {
  // Query the content lake
  client.fetch(
    query,
    { slug },
    { filterResponse: false, lastLiveEventId }
  ).then(
    (res) => {
      // Store the syncTags and "render" the data
      syncTags = res.syncTags
      const data = res.result
      console.log(data)
    })
}

// Kick off initial render
render()

// Subscribe to live updates
const subscription = client.live.events().subscribe(
  (event) => {
    // Check if incoming tags match saved sync tags
    if (event.type === "message" && event.tags.some((tag) => syncTags.includes(tag))) {
      // Refetch with ID to get latest data
      render(event.id)
    }
    if (event.type === "restart") {
      // A restart event is sent when the `lastLiveEventId` we've been given earlier is no longer usable
      render()
    }
})

// Later, unsubscribe when no longer needed (such as on unmount)
// subscription.unsubscribe()

In this example:

  • The example creates a Sanity client instance with the necessary configuration.
  • It defines a query to fetch posts and executes it, setting filterResponse: false to get the syncTags along with the result.
  • It stores the returned syncTags and renders the initial data.
  • It subscribes to live updates using client.live.events().
  • Whenever an update event arrives, it checks whether any of the event's tags match the stored syncTags.
  • If there's a match, it refetches the data, passing the event ID as lastLiveEventId to get the latest version.
  • It updates the stored syncTags and re-renders with the fresh data.
  • Finally, it unsubscribes from the live updates when they're no longer needed.

This pattern keeps your application's content in sync with the latest changes in your Sanity dataset. For additional examples, including listening for drafts, see the JavaScript client documentation.

Next steps

Use live content with a private dataset

The Live Content API works with private datasets, and a connection without a token receives events for published content. The browser doesn't need a token to stay up to date. Events contain only sync tags, which are opaque hashes, and never include document IDs or content, so an anonymous connection can't read anything from the dataset.

Queries behave differently. A query without a token against a private dataset doesn't return an error. It runs as if the dataset were empty: count(*) returns 0, and a document query returns null or []. Fetch content on the server with a token that has viewer access rights or lower, and connect to live events from the browser without one.

In next-sanity, sanityFetch sends serverToken only for draft perspectives and stega-encoded requests. Published queries use the token set on the client you pass to defineLive, so add one there. Read it from an environment variable without the NEXT_PUBLIC_ prefix so Next.js never bundles it for the browser:

import { createClient } from "next-sanity";

import { dataset, projectId } from "../env";

export const client = createClient({
  projectId,
  dataset,
  apiVersion: "2026-03-01",
  useCdn: true,
  // Private datasets need a token for every query, including published content
  token: process.env.SANITY_API_READ_TOKEN,
});

With a token on the client, you can set browserToken: false in defineLive. SanityLive then connects without a token and receives every published change. The browser connection still needs your site's origin in the project's CORS origins. See Origin not allowed by CORS.

Live updates for drafts need a token on the connection, and browserToken sends that token to the browser. To keep it on the server, relay live draft updates through your server.

Sync Tag Invalidate Functions don't deliver updates to browsers. They invalidate caches, such as a CDN, when content changes.

Troubleshooting

client.live.events() reports failures as an error on the observable rather than throwing, so pass an error handler to subscribe to see them at all.

Origin not allowed by CORS

An unlisted origin makes the connection fail without a usable reason, so the client checks the project's CORS configuration and reports a CorsOriginError. In a browser, the message ends with a link that pre-fills the origin: The current origin is not allowed to connect to the Live Content API. Add it here: followed by the URL. On a server, where no origin is available, it reads The current origin is not allowed to connect to the Live Content API. Change your configuration here: followed by the project's API settings URL.

The stream errors and doesn't retry. The client only reports this error when it can confirm the rejection, so an ambiguous check surfaces the underlying connection error instead. Add the origin in the project's API settings at sanity.io/manage.

In a Next.js app, SanityLive logs a warning instead of failing the render: Sanity Live is unable to connect to the Sanity API as the current origin - ORIGIN - is not in the list of allowed CORS origins for this Sanity Project. Set onError="throw" to surface it to the nearest error boundary instead.

Connection rejected by the API

A rejected token produces EventSource connection failed on the observable, with the HTTP status on the error's status property. Any 4xx other than 408 and 429 is fatal: the client stops and doesn't reconnect. A 5xx, a 408, or a 429 is retried, and the stream emits a reconnect event first.

The status property is only populated where the eventsource package provides the connection. Native browser and Node implementations expose no status, so the client can't tell a rejected token from a dropped network and retries instead. A silent reconnect loop with no error is the symptom of an authentication problem in those environments.

A token used to read drafts needs viewer rights or lower. Requesting drafts with no token throws before any request is made: The live events API requires a token or withCredentials when 'includeDrafts: true'. Please update your client configuration. The token should have the lowest possible access role.

Respond to a restart event

A restart event means the lastLiveEventId you hold is no longer usable. Its payload carries only two fields, type and id, and no sync tags.

Handle it in three parts:

  • Refetch every query, and don't pass the event's ID as lastLiveEventId.
  • Discard the sync tags you've stored. They can no longer be matched against incoming events.
  • Treat reconnect the same way. Both events invalidate buffered tags.

In a Next.js app, SanityLive calls router.refresh() on restart by default, so server components re-render with fresh data.

Draft content missing from results

Querying with the published perspective returns published content only, and nothing tells you that's what happened. There's no error and no console message, and the response looks identical to one from a dataset with no drafts. Since API version v2025-02-19, published is the default.

Set perspective: 'drafts' and supply a token to read drafts. In Next.js, defineLive pins its internal client to published, so pass a serverToken to read drafts on the server and a browserToken for live preview in the browser. Without them, defineLive warns in development only.

Pages fail with a cacheTag error

With cacheComponents: true in next.config.ts, every page that calls sanityFetch returns a 500 with `cacheTag()` can only be called inside a `"use cache"` or `"use cache: private"` function. create-next-app 16.4 and later enables Cache Components by default, so a fresh scaffold hits this on the first fetch.

Set cacheComponents: false to use the setup in this guide as written. To keep Cache Components, call sanityFetch from a function marked use cache and resolve draftMode() outside it. Sanity Live with Next.js Cache Components walks through that setup, including the strict option on defineLive that makes the compiler enforce it.

Was this page helpful?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme