Sanity SDK
Framework-agnostic TypeScript core for building custom apps on top of Sanity's real-time content platform.
Repository Health
Technical Analysis
@sanity/sdk is the framework-agnostic core of the Sanity App SDK, a toolkit for building fully custom applications that read and write Sanity content. It packages the business logic behind authentication, document CRUD and lifecycle actions, GROQ-backed queries, presence, comments, releases, and permissions into a single reactive store, so any UI layer can subscribe to live Sanity data without hand-rolling its own sync layer.
Where Sanity Studio bundles this logic into one editing surface, the SDK exposes it as composable functions — getDocumentState, getQueryState, applyDocumentActions, getClientState, and dozens more — each returning a StateSource with synchronous getCurrent(), imperative subscribe(), and RxJS observable access patterns. @sanity/sdk-react builds React hooks on top of this exact core, and the package’s /agent and /comlink sub-entries expose AI-agent actions and cross-window messaging utilities for building embedded dashboard apps.
It targets teams building bespoke internal tools, embedded workflow apps, or dashboard-hosted integrations against Sanity’s Content Lake, where the default Studio UI doesn’t fit but the underlying real-time data model still should.
What You Get
- Instance-scoped stores -
createSanityInstanceplusbindActionByResource/bindActionGloballyisolate state per project/dataset (or share it globally) with automatic disposal-driven cleanup. - Document lifecycle actions -
createDocument,editDocument,publishDocument,discardDocument, and release actions (createRelease,scheduleRelease) applied transactionally viaapplyDocumentActions. - Reactive query and document state -
getQueryState/resolveQueryandgetDocumentState/resolveDocumentreturnStateSourceobjects built for React’suseSyncExternalStore, backed by GROQ and live content sync. - Auth and permissions built in -
getAuthState,setAuthToken,handleAuthCallback, andgetPermissionsState/checkPermissionscover login flows and per-action access checks without extra wiring. - Presence and comments -
getPresence/reportPresenceand the comment-thread APIs (createComment,resolveCommentThreads) bring collaborative-editing primitives out of Studio and into custom apps. - AI agent and cross-window sub-entries -
@sanity/sdk/agentexposesagentGenerate/agentPatch/agentPromptfor AI-assisted content actions, and@sanity/sdk/comlinkprovides typed channel/controller utilities for dashboard-embedded apps.
Common Use Cases
- Custom internal editing tools - a content team builds a purpose-specific editor for one document type instead of exposing the full Studio.
- Dashboard-embedded apps - an app installed into Sanity Dashboard uses
comlinkto communicate with its host frame while reading/writing content through the core store. - Non-React integrations - a team building a Vue, Svelte, or vanilla-JS surface uses the framework-agnostic core directly rather than
@sanity/sdk-react. - AI-assisted content workflows - a product uses the
/agententry point to let an LLM draft or patch Sanity documents throughagentGenerate/agentPatchwith the same permission and document-lifecycle guarantees as manual edits. - Real-time collaborative review surfaces - an app layers presence and comment threads on top of live document state to mirror Studio’s collaborative editing inside a custom UI.
Under The Hood
Architecture
The SDK is organized as independently-scoped “stores” (auth, client, document, query, presence, users, releases, comments, etc.), each defined via defineStore and instantiated per key through createStoreInstance. createActionBinder is the load-bearing abstraction: bindActionByResource, bindActionByResourceAndPerspective, and bindActionGlobally all derive from one generic binder that keys store instances by project/dataset/perspective (or globally, for stores like auth), registers each consuming SanityInstance for disposal tracking, and tears the underlying store down once the last instance unsubscribes. Every store action ultimately reads/writes through this same binder, so document, query, and presence state all share one lifecycle and disposal model despite being logically separate stores. createSanityInstance itself is a lightweight, disposable handle carrying config and a listener registry, not a heavyweight client object.
Tech Stack
Written in strict TypeScript against Node/browser targets, using Zustand’s vanilla createStore (with devtools middleware) for the underlying mutable state container and RxJS for the reactive surface layered on top via createStoreState’s observable. @sanity/client provides the HTTP/CDN transport (wrapped by clientStore, which reconfigures clients automatically on token or auth-method changes), while groq/groq-js power query parsing and reselect-style selector patterns drive derived state. The monorepo is Turborepo/pnpm-workspace managed, built per-package with @sanity/pkg-utils and Vite, and ships multiple explicit sub-entry points (., /agent, /comlink, /_internal) rather than one flat export surface.
Code Quality
Test coverage is extensive and consistent: nearly every source module in packages/core/src (auth, document, query, presence, releases, users, utils, etc.) ships a co-located .test.ts, with additional .test-d.ts type-level tests for public API shapes and dedicated concurrency tests for the document store. The TypeScript config extends a shared strictest preset (strictNullChecks, noImplicitAny, noImplicitReturns, noImplicitOverride, and more all enabled), ESLint and a custom formatter (oxfmt) run repo-wide, and GitHub Actions runs dedicated lint, type-check, test, and end-to-end-test workflows on every change. JSDoc comments consistently tag exports @public or @internal, which doubles as the contract enforced by the package’s generated API surface.
What Makes It Unique
The core abstraction — a StateSource exposing synchronous getCurrent(), imperative subscribe(), and an RxJS observable from the same underlying value — is deliberately designed so React’s useSyncExternalStore and manual/reactive consumers can share one implementation with no duplicated sync logic. Combined with the generic action-binder pattern that scopes any store to a resource, a resource+perspective, or globally, the SDK lets one store definition serve isolated per-dataset state and shared global state (like auth) through the same mechanism, rather than hand-writing separate caching and lifecycle code for each concern.