@sanity/mutate
A TypeScript toolkit for building, encoding, and applying Sanity content mutations, with an optimistic local store for real-time editing UIs.
Repository Health
Technical Analysis
@sanity/mutate is a low-level TypeScript toolkit from Sanity.io for constructing, encoding, and applying content mutations against Sanity’s Content Lake API. It exposes declarative, composable mutation creators — create, patch, createIfNotExists, createOrReplace, and delete_ — along with a rich set of node-level patch operations (set, unset, insert, diffMatchPatch, and more) that can be applied atomically at any path in a document.
Beyond building mutations, the library ships a pure, in-memory apply engine for computing the resulting document state without a network round-trip, plus an optimistic local store with rebase semantics for building real-time and offline-capable editing experiences. This makes it well suited for visual editors, collaborative CMS interfaces, and any application that needs to predict, merge, or replay Sanity mutations locally before — or instead of — sending them to the server.
What You Get
- Declarative mutation creators (create, patch, createIfNotExists, createOrReplace, delete_) for building fully-typed mutation objects
- A full set of node-level patch operations — set, unset, insert, diffMatchPatch, truncate, assign, and more — addressable at any document path
- A pure, dependency-free apply engine that computes the resulting document state from a list of mutations without hitting the network
- An optimistic local store with rebase semantics for merging in-flight local edits against confirmed server mutations
- Sanity-compatible and compact encoders/decoders for translating between the library’s internal mutation format and Sanity’s HTTP mutation wire format
Common Use Cases
- Building visual/real-time editing UIs that apply and preview mutations locally before committing them to Sanity
- Implementing optimistic updates in collaborative Sanity Studio plugins, with automatic rebase when the server confirms or rejects changes
- Constructing complex, typed patch operations programmatically (e.g. bulk content migrations) instead of hand-writing raw Sanity patch JSON
- Powering offline-first or intermittently-connected apps that need to queue and replay Sanity mutations once connectivity is restored
Under The Hood
Architecture @sanity/mutate is organized into clearly separated layers: mutations/ holds declarative creators and the typed Operation/NodePatch definitions that everything else builds on; path/ owns a small purpose-built path parser and getter (parse/stringify); apply/ contains a pure, side-effect-free patch-application engine that resolves NodePatch operations against a plain document; encoders/ handles translation to and from Sanity’s wire format (sanity, compact, form-compat variants); and store/ is the only stateful layer, composing RxJS (Subject/Observable, merge/scan/concatMap) in its optimistic-store implementation to track three states per document — base (server-confirmed), inflight (submitted but unconfirmed), and local (unsubmitted) — while a dedicated rebase module implements the trickiest piece: converting pending string-set patches into diff-match-patch form against the old base, replaying them against a new base with an explicit fallback to raw patches on failure, then rewriting the resulting set values. The core NodePatch/Operation type is the load-bearing abstraction — apply, rebase, the optimistic store, and both encoders all depend on its shape.
Tech Stack The package is TypeScript-only (strict mode, with noUncheckedIndexedAccess and noImplicitAny enabled) and ships as ESM, built with Sanity’s own pkg-utils build tool in strict/check mode. Tests run on Vitest with type-checking enabled alongside runtime checks, plus coverage tooling; linting uses ESLint’s flat config with typescript-eslint, import sorting, and Prettier integration; git hooks are managed with lefthook and releases are automated via release-please. Runtime dependencies include rxjs (the optimistic store’s reactive core), @sanity/client, @sanity/diff-match-patch, @sanity/uuid, mendoza (Sanity’s binary document-diff/effects format), hotscript (type-level utilities), lodash, nanoid, and a TTL cache library, with xstate as an optional peer dependency used only by the document-mutator state machine. The repository is a pnpm workspace containing the library plus a documentation site and example apps.
Code Quality Test coverage is extensive and granular, reaching nearly every module — patch application, path parsing, both encoders, and especially the optimistic store, which has dedicated suites for rebase, concurrent editing, transactions, submit-once semantics, and squash optimizations. Because tests run with type-checking enabled, type-level behavior is verified alongside runtime behavior, and strict TypeScript settings catch a class of bugs many libraries at this scale don’t bother enforcing. Error handling is explicit rather than swallowed — the patch-mutation applier throws typed errors on revision or id mismatch, and the rebase logic wraps diff-match-patch replay in try/catch with an explicit fallback path. Separate GitHub Actions workflows for linting, type-checking, and testing (matrixed across Node LTS versions) gate every push and pull request to main, backed by lefthook for local pre-commit enforcement.
API Design The public API favors composability and strong type inference over configuration: create, patch, at, and the operation creators (set, unset, insert, diffMatchPatch, and more) read like a small typed DSL, with generics propagating precise mutation shapes into the return types of patch() and at() so most mistakes surface at compile time. Getting started needs almost no boilerplate — a single patch() call with an at()/set() pair is a valid mutation — while heavier integrations, like the optimistic store and the XState-based document mutator machine, are opt-in via explicit factory functions rather than imposed on every consumer. Documentation lives on a dedicated docs site generated from the repo, with guides for getting started, applying mutations, the optimistic store, recipes, and an explicit “differences from the Sanity API” page — an unusual amount of naming-clarity effort for a low-level library, though several unstable-prefixed exports signal parts of the surface are still being finalized.