Type something to search...
Real-Time Collaboration in Sanity Explained

Real-Time Collaboration in Sanity Explained

Real-time collaboration in Sanity means several people can edit the same document at the same time without locking it or overwriting each other's work. Instead of saving whole documents, Sanity Studio sends small, granular changes called patches to the Content Lake as you type. The Content Lake applies them in order and streams them to everyone else with the document open, so each editor's screen stays in sync within moments. Presence avatars show who is viewing and which field they're in, every change lands in a shared draft and the document history, and on paid plans comments and tasks let the team discuss and assign work in the same place.

This is one of the features that sets Sanity apart from traditional CMSs, where two editors opening the same page usually means one of them loses their changes. Understanding how it works helps editors trust it and helps developers avoid breaking it with custom code. This article covers the patch-based model, what editors see, how drafts behave in a team, how conflicts are avoided, comments and tasks, what developers need to know about custom inputs and scripts, and how to use the same real-time stream in your own apps.

Save-Based Editing vs Patch-Based Editing

Most content systems follow a save model. You load a document, edit a local copy and send the whole thing back when you click Save. If someone else saved in the meantime, the last save wins or you get a conflict screen.

Sanity works differently:

  1. The Studio loads the document from the Content Lake.
  2. As you edit, it describes each change as a mutation, such as "set title to this value" or "insert this item after the item with key a1b2".
  3. It sends those mutations to the Content Lake, which applies them to the stored document.
  4. The Content Lake streams the mutations to every other client listening to that document.
  5. Each Studio applies the incoming mutations to its local copy.
Save-based CMSSanity
What's sentThe whole documentSmall patches for the fields that changed
WhenWhen you click SaveContinuously, as you type
Two editors at onceLocking, or last save winsBoth edit; changes merge field by field
Seeing others' changesAfter a reloadLive, within moments

Because changes are granular, two people editing different fields never interfere with each other, and even edits to the same text field are merged where possible.

What Editors See

Presence

When someone else opens a document you're viewing, their avatar appears in the document header. As they move between fields, a small avatar appears next to the field they're focused on, so you can see at a glance that a colleague is working on the SEO description while you write the body. Presence also shows in document lists, so you know a document is being edited before you open it.

Live Changes

Text, references, images and array items update on your screen as the other person edits. There's no reload and no "this document has changed" warning, because your copy is never out of date for long.

No Save Button

Every change is stored as soon as it's made. Closing the tab doesn't lose work, and pressing Ctrl+S or ⌘+S only shows a reminder that changes save automatically. The Studio walkthrough shows these features in context.

Drafts in a Team

Real-time editing happens in a draft: a copy of the document with an ID starting with drafts.. The published document doesn't change until someone clicks Publish. In a team this has a few consequences worth knowing:

  • There's one shared draft per document. Everyone editing the document edits the same draft. There are no personal branches.
  • Publishing publishes everyone's changes. If a colleague is halfway through rewriting a section when you publish, their half-finished edit goes live too.
  • Discarding changes discards everyone's. "Discard changes" deletes the shared draft.

So agree on simple conventions, such as one person owning publication for each document, and use the presence avatars to check before you publish. For larger pieces of work, Content Releases let you stage changes to many documents in a separate version and publish them together, without touching the regular draft. Releases are listed as an Enterprise feature; the pricing guide has the details. The Content Lake explained post describes how drafts, versions and published documents are stored.

How Conflicts Are Avoided

Sanity avoids conflicts by making changes small and precise rather than by locking.

  • Field-level patches: setting title doesn't touch body, so two editors on different fields never collide.
  • Text diffs: for strings, the Studio sends a diff of what changed rather than the whole value, so concurrent edits to different parts of the same text can both survive.
  • Keyed array items: every object in an array has a _key. Patches target items by key, not by position, so inserting an item while someone else edits another doesn't shift their edit to the wrong item.
  • Ordered transactions: the Content Lake applies mutations in sequence, so every client ends up with the same result.

When two people change exactly the same value at the same moment, the later change wins for that value. In practice this is rare, because presence makes it obvious when someone else is in the field.

History and Attribution

Every patch is recorded with who made it and when. The history panel shows a timeline of changes, and you can view or restore the document at any earlier point. When something unexpected appears in a document, history tells you who changed it, which matters more when several people edit at once. How far back history goes depends on your plan; see document history and revisions.

Comments and Tasks

Real-time editing covers working on the same content. Comments and tasks cover talking about it. Both are available on the Growth plan and above, and are enabled by default on eligible projects.

  • Comments attach to a specific field, or to selected words in Portable Text. You can @mention colleagues, who get a notification, react, and resolve threads when they're done.
  • Tasks are assignments with a due date and an assignee, optionally linked to a document. They suit work like "add images to this post by Friday".

Both are stored in an add-on dataset next to your content, which doesn't count towards your plan's document limits. You can turn comments off, or limit them to certain types, in sanity.config.ts:

// sanity.config.ts
import { defineConfig } from 'sanity'

const COMMENTABLE = ['post', 'page', 'whitepaper']

export default defineConfig({
  // ...projectId, dataset, plugins, schema
  document: {
    comments: {
      enabled: (context) => COMMENTABLE.includes(context.documentType),
    },
  },
  tasks: { enabled: true },
})

If you use custom roles, make sure every role that should use comments and tasks can read project details, project members and the project's datasets, as well as the documents themselves. Otherwise mentions and comment counts won't work. The roles and permissions guide covers role setup.

Writing Real-Time-Safe Custom Inputs

Custom input components are where developers most often break collaboration. An input that sends the whole object value on every change works for one editor, but with two people it overwrites the other person's changes to sibling fields.

Instead, emit precise patches using the helpers exported by sanity:

// components/DimensionsInput.tsx
import { Stack, TextInput, Text } from '@sanity/ui'
import { set, unset, type ObjectInputProps } from 'sanity'

type Dimensions = { width?: number; height?: number }

export function DimensionsInput(props: ObjectInputProps<Dimensions>) {
  const { value, onChange } = props

  const update = (field: keyof Dimensions) => (event: React.ChangeEvent<HTMLInputElement>) => {
    const next = event.currentTarget.value
    // Patch only the field that changed, by path
    onChange(next === '' ? unset([field]) : set(Number(next), [field]))
  }

  return (
    <Stack space={3}>
      <Text size={1}>Width</Text>
      <TextInput type="number" value={value?.width ?? ''} onChange={update('width')} />
      <Text size={1}>Height</Text>
      <TextInput type="number" value={value?.height ?? ''} onChange={update('height')} />
    </Stack>
  )
}

The rules of thumb:

  • Patch the smallest path you can: set(value, ['width']), not set({ ...value, width }).
  • Target array items by key: use paths like [{ _key: item._key }, 'title'], never an index.
  • Use relative operations where they match intent: inc and insert don't depend on the value you last saw, so they're safe when others are editing.
  • Let the Studio handle text diffs: for strings, set() is fine. The Studio optimises text changes for you.

The custom input components guide covers building inputs in full.

Scripts and Concurrent Edits

Scripts and integrations write to the same documents editors are working on. With @sanity/client, prefer targeted patches over replacing whole documents:

// scripts/mark-reviewed.ts
import { createClient } from '@sanity/client'

const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID!,
  dataset: 'production',
  apiVersion: '2025-02-19',
  token: process.env.SANITY_WRITE_TOKEN,
  useCdn: false,
})

// Safe: only touches two fields, leaves everything else alone
await client
  .patch('drafts.post-123')
  .set({ reviewStatus: 'approved' })
  .setIfMissing({ reviewHistory: [] })
  .commit()

When a script must not overwrite changes it hasn't seen, use the revision ID as a guard. The patch fails if the document has changed since _rev was read:

const doc = await client.getDocument('post-123')

await client
  .patch('post-123')
  .ifRevisionId(doc!._rev)
  .set({ title: doc!.title.trim() })
  .commit()

If the commit fails because the revision changed, fetch the document again and retry. The mutation API guide covers patches and transactions in more depth.

Using the Real-Time Stream in Your Own Apps

The same listener system that powers the Studio is available to your code. client.listen() opens a server-sent events connection and emits an event for each change to documents matching a GROQ filter:

const subscription = client
  .listen('*[_type == "post"]', {}, { includeResult: true })
  .subscribe((event) => {
    if (event.type === 'mutation') {
      console.log(`${event.documentId} changed (${event.transition})`)
    }
  })

// Later
subscription.unsubscribe()

Listener queries only support filters: projections, ordering and joins are ignored. Events can arrive out of order, so compare previousRev and resultRev, or rely on the latest result, if order matters.

For websites, you usually don't need raw listeners. The Live Content API, used by defineLive in next-sanity, keeps pages up to date as content is published, and live preview shows drafts as editors type. To run server-side code when content changes, use webhooks or Sanity Functions instead.

What's Available on Each Plan

FeatureFreeGrowthEnterprise
Real-time co-editing and presenceYesYesYes
Document historyShorter retentionLonger retentionLongest retention
Comments and tasksNoYesYes
Content ReleasesNoNoYes

Check the current pricing page before relying on a plan feature, since packaging changes from time to time.


FAQ: Real-Time Collaboration in Sanity

Yes. Sanity Studio sends each change as a small patch and streams it to everyone with the document open, so several editors can work on the same document at once without locking or overwriting each other.

Different fields never conflict. For text, the Studio sends diffs so changes to different parts of the same text can both survive. If two people set exactly the same value at the same moment, the later change wins for that value.

Yes. There's one shared draft per document, so Publish publishes everything in it, including a colleague's work in progress. Check presence avatars before publishing, or use Content Releases for larger staged changes where your plan includes them.

Yes. Real-time co-editing and presence work on every plan. Comments and tasks require the Growth plan or above.

It can, if it replaces whole object values on every change. Emit precise patches with set, unset and insert, target array items by _key, and the input will behave correctly when several people edit together.

Yes. client.listen opens a stream of changes for a GROQ filter, and the Live Content API, used by defineLive in next-sanity, keeps pages updated as content is published.


Conclusion

Real-time collaboration in Sanity rests on one idea: send small, precise changes instead of whole documents. Patches are applied in order by the Content Lake and streamed to every open Studio, presence shows who's where, and history records who did what. Comments and tasks, on paid plans, add discussion and assignments in the same interface.

For editors, the main thing to remember is that a draft is shared, so coordinate before publishing. For developers, keep collaboration intact by writing custom inputs and scripts that patch the smallest path possible, guard risky writes with revision IDs, and use listeners, the Live Content API or webhooks when your own apps need to react to the same stream of changes.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper