Discussion about drafts causing overwriting of backend updates in Sanity

20 replies
Last updated: Sep 10, 2020
Hi, we have a problem with drafts. By using the history api trying to make sense of that happens, it looks like our problems are caused by editors publishing drafts after backend token has updated docs, and thus overwriting it. Here’s what I think is going on:1. An editor starts editing a document, could be by accident, and then forgets to publish it.
2. Our server updates the document one or more times
3. Some editor that comes across the doc, publishes the draft.
It looks like this process
causes us to loose all the patches from step 2. And the time from 1-3 may be a few seconds (not likely for anything to happen at step 2), or days (more likely that our server did some work at step 2).
• I could encourage all editors to discard all drafts that they come across, and don’t publish anything that they haven’t edited the last few seconds.
This should keep the changes from step 2, right?• If editors are encouraged to keep drafts open only for a short period of time before publishing, the probability of something else being written to the doc in the meantime is very low. However, there’s still a small chance that step 2 will happen even though an editor is working with the doc only for a second or two, before publishing.
So what’s how do we prevent editors from overwriting updates our backend did?
AI Update

You've identified the problem correctly – this is a classic race condition where editors publishing drafts can overwrite backend updates. The good news is that Sanity has built-in mechanisms to prevent exactly this scenario.

The Solution: Use ifRevisionID for Optimistic Locking

The best way to prevent your backend updates from being overwritten is to use optimistic locking with the ifRevisionID parameter in your backend mutations. Here's how it works:

When your backend updates a document, it should:

  1. First fetch the current document (including its _rev field)
  2. Apply your changes using a patch mutation with ifRevisionID set to the current _rev value
// Example using the JS client
const doc = await client.getDocument(documentId)

await client
  .patch(documentId)
  .set({ yourField: newValue })
  .ifRevisionID(doc._rev)  // Only apply if revision matches
  .commit()

If someone else (an editor or another backend process) has modified the document in the meantime, the mutation will fail with a 409 Conflict HTTP status code. Your backend can then:

  • Fetch the updated document
  • Reapply your changes with the new data
  • Retry with the fresh _rev

Why This Works

According to the transactions documentation, Sanity uses exclusive locks during transactions to provide repeatable read isolation. However, the pattern you're experiencing (read-then-write cycles) doesn't have the same isolation guarantees unless you use optimistic locking.

The ifRevisionID parameter ensures that your mutation is only accepted if the document hasn't changed since you read it. This prevents the "lost update" anomaly you're experiencing where one writer's changes overwrite another's.

Additional Considerations

For your editors: You mentioned encouraging them to discard drafts – while that could help, it's not a reliable solution since it depends on human behavior. The technical solution with ifRevisionID is much more robust.

Draft vs Published: Remember that in Sanity, drafts and published documents are separate (drafts have the drafts. prefix). When an editor publishes a draft, it's essentially copying the draft over the published document. If your backend is updating the published document while a draft exists, publishing that draft will indeed overwrite those changes unless you use ifRevisionID.

Eventual Consistency: Be aware that Sanity has eventual consistency for queries (though transactions themselves are strongly consistent). If your backend relies on GROQ queries to determine what to update, there could be a small delay before changes are visible to queries.

This approach will ensure your backend updates are never silently overwritten, and you'll have clear error handling when conflicts occur.

Hi Stian, this is an interesting question. The use case is valid but unfortunately it’s also a limitation of our draft model. Inside the Studio, it won’t allow you to edit the original so this issue would not occur. However, as you suggest, this is not the case for external mutations.
Rather than asking editors to tiptoe around the Studio and avoid using drafts (thereby limiting their workflow), I would suggest trying to fix this on the server side instead. Nevertheless, there is a document property called
liveEdit
that disables drafts altogether if you prefer to take that route.
Regarding the server patches, are these changes to fields editable by users or rather `readOnly`/`hidden` fields? You could consider checking for the existence of a draft version and, if so, patch both the draft and the non-draft document. This would go around the issue even if editors subsequently publish or discard the draft. However, it could lead to unexpected results if it changes fields that editors thought to have updated in the draft - hence the question on these fields being shielded
🙂
Quick question first; there can only be one draft for each document, right? And if more than one editor is editing the document at the same time, they are editing the same draft?
So the server patches change all kinds of fields, hidden, read only, etc. And what happens is that the editor publishing a draft will ignore the changes on the backend “branch” of changes. So as long as a document is opened for editing, all backend changes will be overwritten when it’s published, and kept if Studio changes are discarded. That’s at least what it looks like.

I think our best option is to try
liveEdit
. Because I don’t really want to deal with the complexity of having to update multiple documents… To us that would be supporting an edge case. It’s better to drop the idea of drafts.
But I need to think through what happens if editors update documents with invalid data. I guess that if the input does not pass validation, the patch will be rejected? It’s more a question of can I / will I be able to write correct and up to date validation functions for all fields.
Hmm, looks like when using
liveEdit: true
, the document is saved even though it does not pass validation…. If that’s the case, we cannot use live edit
Indeed, there’s only one draft (when there’s a draft). And the behaviour you describe makes sense. The draft is created from the original and then the original is changed by the server patches, so the draft being published overwrites those changes to the original.
Patching both the original and the draft version could mitigate this, but that might also overwrite changes an editor had in mind for a (not hidden/read-only) field.

Regarding your pre-last comment, if you set up validation in the Studio this should prevent editors from publishing invalid input. If you’re referring to editors being able to push server patches outside of the Studio, then you’d have to take care of validation on that side as well.

Let me check the
liveEdit
validation issue - that’s unexpected indeed.
Looks like we cannot browse history in live edit mode either. But I’m going to merge a branch where we updated to the newest version of Sanity. We may be a bit behind
Updated to newest version, and live mode still writes invalid input
Ok, so our solution for now will be to ask all editors to discard drafts they come across and haven’t worked with the last minute or so. We usually do pretty small changes, so a minute should be sufficient. Then a conflict is at least less likely.
And, also I will write a cron job that spams our general channel with open drafts…

My biggest wish from you guys right now is a way to let editors know that the document has a newer revision set by some other user = backend token during edit. Then they could stop editing, and start from the newest version.
That last part is probably something that can be achieved using custom document actions: e.g. on publish run a check for the time the draft document was created (query history API for example) and compare it to the
_updatedAt
of the original doc. If the latter is more recent, there have been changes and you could not publish but show an alert instead.
Hmm, what are custom document actions? Documented somewhere, or do I have to write my own publish component?
Yeah, found it. I’ll look into it
Thanks
Looks like
props.draft._createdAt
is when the original doc was created. And
props.draft._updatedAt
will update when the draft changes, so the draft will always be fresher than the original (Unless it’s an old draft, but input one character and it’s fresher). You mentioned the history API, will I get other value for the draft’s
_createdAt
there?
Looks like I won’t get unpublished drafts from the history API anyways
Nevermind, if I get the draft’s history I get the last draft’s timestamp
/transactions/drafts.<id>
It kinda sucks that I have to visit the history API to figure out if I can publish a doc or not. It would have been super helpful if this was something that came out of the box.
I’ll implement a solution because we really need this. If it’s a quick fix, providing the draft creation time in document actions props will save people a lot of pulling in axios, leaving a token there (cause you can’t get history from the client, right?) and dealing with ndjson.
Indeed, getting draft creation directly makes a lot of sense for this use case. Also, having an option to patch drafts automatically, similar to
createIfNotExists
on the mutation API, would be useful here. Thanks for the feedback in this thread Stian. I’ll make sure to forward this to the people involved in projects related to the above.
However, I guess the good news is that despite the need to go around this stuff it seems like we have a way to alert editors in case changes happened while they were editing a document. I hope in the future we’ll be able to offer some more functionality around this, perhaps even going around the need for custom actions.
yeah, hope so too! Thanks for all help!
Hmm, I don’t know about this. It seems that the request to history api is not guaranteed to get the current draft, so I would have to set a delay for a second or so. Also, after visiting the original PublishAction component (https://github.com/sanity-io/sanity/blob/fae42f5e0b5f09e725a3fbdde6ed1580b3a98d67/packages/%40sanity/desk-tool/src/actions/PublishAction.tsx ), I noticed that there is actually a lot of useful logic going on there, like validation. It’s a 113 lines long, quite messy, file and copying it and adding revision checks would make it even more messy, and I would have to make sure my altered component and the original one has the same base functionality when upgrading sanity.
I really feel this is core revision functionality and you should not let editors overwrite more recent versions of the same document.

It would be way more solid if you solved this on your end. A custom component written by someone who does not know the intention behind the original PublishAction, will deviate from the intended functionality over time, get outdated on upgrades and probably introduce bugs.

Are you planning on fixing the drafts model any time soon? It should be aware of other branches of the same document, and show conflict to editors if necessary, not just overwrite data.

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?