
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:
- The Studio loads the document from the Content Lake.
- As you edit, it describes each change as a mutation, such as "set
titleto this value" or "insert this item after the item with keya1b2". - It sends those mutations to the Content Lake, which applies them to the stored document.
- The Content Lake streams the mutations to every other client listening to that document.
- Each Studio applies the incoming mutations to its local copy.
| Save-based CMS | Sanity | |
|---|---|---|
| What's sent | The whole document | Small patches for the fields that changed |
| When | When you click Save | Continuously, as you type |
| Two editors at once | Locking, or last save wins | Both edit; changes merge field by field |
| Seeing others' changes | After a reload | Live, 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
titledoesn't touchbody, 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']), notset({ ...value, width }). - Target array items by key: use paths like
[{ _key: item._key }, 'title'], never an index. - Use relative operations where they match intent:
incandinsertdon'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
| Feature | Free | Growth | Enterprise |
|---|---|---|---|
| Real-time co-editing and presence | Yes | Yes | Yes |
| Document history | Shorter retention | Longer retention | Longest retention |
| Comments and tasks | No | Yes | Yes |
| Content Releases | No | No | Yes |
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.


