
Sanity Best Practices for Production Projects
The most important Sanity best practices come down to a handful of habits: model content by meaning and validate it, write GROQ queries with explicit projections, pick the CDN or the live API on purpose, keep tokens on the server, separate environments with datasets, treat schema changes like database migrations, and deploy the Studio from version control. None of these are difficult on their own. The projects that run smoothly are the ones that apply them from the first week instead of retrofitting them after launch.
This guide is a project-wide roundup rather than a deep dive. Each section gives you the rules that matter and links to a detailed post when you want the full picture. It covers schema and content modelling, querying, performance and caching, security, environments and migrations, Studio deployment, editorial workflow, and team process, then finishes with a launch checklist you can copy.
Schema and Content Modelling
Your schema decides how pleasant the Studio is, how simple your queries are and how painful the next redesign will be. The short version:
- Model content, not pages: create types for the things your organisation talks about (products, people, events), and let pages display them.
- Use references for shared data: if two documents need the same information, it belongs in its own document.
- Name fields by meaning:
tagline, notbigRedText. Field names are hard to change once content exists. - Use
defineTypeanddefineField: they give you autocompletion and catch typos at build time. - Validate everything that matters: required titles, unique slugs, alt text on images, maximum lengths for SEO fields.
The details, including when to choose documents, objects or arrays, are in content modelling best practices in Sanity.
Validation is the part teams skip most often, and it's the cheapest protection you have:
// schemaTypes/post.ts
import { defineField, defineType } from 'sanity'
export const post = defineType({
name: 'post',
title: 'Post',
type: 'document',
fields: [
defineField({
name: 'title',
type: 'string',
validation: (rule) => rule.required().max(90),
}),
defineField({
name: 'slug',
type: 'slug',
options: { source: 'title', maxLength: 96 },
validation: (rule) => rule.required(),
}),
defineField({
name: 'mainImage',
type: 'image',
options: { hotspot: true },
fields: [
defineField({
name: 'alt',
type: 'string',
validation: (rule) => rule.required().warning('Alt text helps accessibility and SEO'),
}),
],
}),
defineField({ name: 'publishedAt', type: 'datetime' }),
],
})
Remember that validation runs in the Studio only. Documents written through the API or a migration script skip it, so your front end should still handle missing values. How to add validation rules covers custom and async rules.
Querying With GROQ
Good queries are small, typed and predictable.
| Practice | Why it matters |
|---|---|
| Project explicit fields on every query | Smaller payloads and precise types |
| Separate list and detail queries | Cards don't need the full body |
| Use parameters, never string concatenation | Prevents injection and helps caching |
Wrap queries in defineQuery | TypeGen can generate result types |
Pin a dated apiVersion | Behaviour doesn't change under you |
Filter on defined(slug.current) for routable content | Avoids broken links and null paths |
A query following those rules looks like this:
// src/sanity/queries.ts
import { defineQuery } from 'next-sanity'
export const POST_QUERY = defineQuery(`
*[_type == "post" && slug.current == $slug][0]{
_id,
title,
"slug": slug.current,
publishedAt,
"author": author->{ name, "slug": slug.current },
mainImage { alt, asset },
body
}
`)
Then run TypeGen whenever schemas or queries change:
npx sanity schema extract
npx sanity typegen generate
The habits behind this are covered in using projections in GROQ queries, GROQ parameters and generating types with Sanity TypeGen. For listing pages with many results, use cursor-based pagination rather than large offset slices.
Performance and Caching
Sanity is fast by default, but how you fetch decides how fast your site feels and how much of your quota you use.
Choose the Right Endpoint
Use the API CDN (useCdn: true) for high-traffic, read-only requests from browsers or serverless functions. Use the live API (useCdn: false) for static builds, webhook-triggered revalidation and anything that must see a change immediately. The trade-offs, and why mixing them up causes stale pages, are explained in Sanity CDN vs live API.
Cache in Your Framework, Revalidate on Publish
Don't fetch from Sanity on every request if the content changes a few times a day. Cache responses in your framework, tag them by document type, and let a webhook clear the right tags when content is published:
const posts = await client.fetch(POSTS_QUERY, {}, {
next: { tags: ['post'], revalidate: 3600 },
})
The webhook handler, tag strategy and time-based fallback are in improving site performance with Sanity caching, with webhook setup in how to use Sanity webhooks. If you prefer to skip cache management, the Live Content API through defineLive in next-sanity keeps pages fresh automatically; the next-sanity guide shows both approaches.
Serve Images Through the Image Pipeline
Never render the original asset URL directly. Request the width you need, let Sanity pick the format with auto('format'), and respect the editor's hotspot and crop. How to optimise Sanity images covers sizes, srcset and next/image loaders.
Security
Most Sanity security incidents are self-inflicted, and almost all of them involve tokens.
- Keep tokens on the server: never put a token in a variable prefixed with
NEXT_PUBLIC_,PUBLIC_orVITE_. - Use least-privilege robot tokens: a Viewer token for preview, an Editor token only for scripts that write.
- One token per purpose: if one leaks, you revoke it without breaking everything else.
- Keep CORS origins tight: only allow credentials for the origins that host your Studio.
- Make datasets private when content isn't public: public datasets can be read by anyone who knows the project ID.
- Verify webhook signatures: use
@sanity/webhookto check that requests really come from Sanity.
// Server-only client with a read token
import 'server-only'
import { client } from './client'
export const previewClient = client.withConfig({
token: process.env.SANITY_API_READ_TOKEN,
useCdn: false,
perspective: 'drafts',
})
The full walkthrough is in how to secure your Sanity project, and team access is covered in Sanity roles and permissions.
Environments, Migrations and Backups
Treat your content as production data, because it is.
Separate Environments With Datasets
Run development and staging against their own datasets, and keep production for real content. Configure the dataset through environment variables so the same code works everywhere:
// sanity.config.ts
import { defineConfig } from 'sanity'
export default defineConfig({
projectId: process.env.SANITY_STUDIO_PROJECT_ID!,
dataset: process.env.SANITY_STUDIO_DATASET ?? 'development',
// ...
})
Managing datasets and environments covers naming, copying and aliases.
Change Schemas With Migrations
Renaming or restructuring a field doesn't move existing data. Add the new field, migrate content with npx sanity migration create, update the front end, then deprecate the old field. Always do a dry run against a copy of production first. Sanity content migrations has the full workflow, and common Sanity mistakes shows what happens when this step is skipped.
Back Up What You Can't Recreate
Document history helps you undo a bad edit, but it's not a backup. Schedule regular npx sanity datasets export runs and store the files somewhere you control. Sanity backup and restore shows how to automate it, and document history and revisions explains what history can and can't recover.
Studio Configuration and Deployment
The Studio is part of your codebase, so give it the same care as your front end.
- Keep it in version control: schemas, structure, plugins and config all belong in Git.
- Organise schemas by type: one file per document type, with shared objects in their own folder. See structuring a Sanity project.
- Build a clear desk structure: group content, hide system types and add singletons with the Structure Builder.
- Deploy from CI, not laptops: run
npx sanity deploy(or deploy your embedded Studio with the app) from your pipeline after tests pass. - Deploy the schema too:
npx sanity schema deploystores it in the Content Lake so tools such as the MCP server, Canvas and Agent Actions understand your content. - Pin your Sanity versions: upgrade deliberately, and read the changelog before major versions.
# .github/workflows/studio.yml (excerpt)
- run: npm ci
- run: npx sanity schema validate
- run: npx sanity deploy
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_DEPLOY_TOKEN }}
Hosting options and CI tokens are covered in how to deploy Sanity Studio, and the commands you'll use day to day are in Sanity CLI commands.
Editorial Workflow and Editor Experience
A Studio that editors understand produces cleaner content than any validation rule.
- Write field descriptions: explain what each field does on the site.
- Group long forms: put SEO and settings in their own tabs with field groups.
- Give every type a useful preview: title, subtitle and thumbnail.
- Add preview links or Visual Editing: editors catch problems when they can see the page. See Visual Editing in Next.js.
- Use releases and scheduling for timed content: rather than asking someone to publish at midnight. Scheduled publishing in Sanity explains the options.
- Add approval steps where you need them: custom document actions can gate publishing. See editorial workflows in Sanity.
Custom inputs are worth building when a standard field is genuinely awkward, such as a colour picker tied to your design tokens. Keep them rare and follow the patterns in custom input components.
Team Process
Some best practices aren't technical at all.
| Area | Practice |
|---|---|
| Access | Give each person the lowest role that lets them work, and remove leavers promptly |
| Ownership | Name one person responsible for schema changes and migrations |
| Reviews | Review schema changes in pull requests like any other code |
| Documentation | Keep a short README describing types, datasets, tokens and webhooks |
| Plugins | Prefer maintained plugins, and pin their versions |
| Monitoring | Watch API usage in the project dashboard so quota surprises don't happen |
| Onboarding | Sit with new editors for their first few documents |
When a plugin doesn't exist for something you need repeatedly across projects, package it yourself with the patterns in creating a Sanity plugin.
A Launch Checklist
Before a Sanity project goes live, run through this list:
- Every routable type has a required, unique slug.
- Images require alt text, or at least warn when it's missing.
- Production queries use projections and parameters, with types from TypeGen.
- The CDN is used for public reads and turned off for builds and revalidation.
- No token is exposed in a client bundle. Check the built JavaScript.
- CORS origins list only the domains that need access.
- Webhooks verify their signatures and revalidate the right tags.
- Drafts can't reach the public site, and preview requires Draft Mode.
- The Studio and schema are deployed from CI.
- A scheduled export runs and you've tested a restore.
FAQ: Sanity Best Practices
Model your content by meaning rather than page layout. A good model makes queries simpler, survives redesigns and lets content be reused across channels. Most other problems, from slow queries to painful migrations, trace back to the content model.
Yes, for high-traffic public reads where a short delay is acceptable. Turn it off for static builds, webhook-triggered revalidation, preview and anything that must reflect a change immediately, because the CDN can briefly serve the previous version.
For any project with real content, yes. A development dataset lets you test schema changes and migrations without risking published content. Use environment variables so the same code points at the right dataset in each environment.
No. History lets you inspect and restore earlier versions of individual documents for a limited time that depends on your plan. It won't help if a dataset is deleted or a script damages thousands of documents, so schedule regular exports as well.
Treat them as migrations. Add the new field alongside the old one, migrate the data with a script, update the front end, then deprecate and remove the old field. Test the migration on a copy of production first.
Only on the server, in environment variables without a public prefix. Use robot tokens with the lowest role that works, create one per purpose, and rotate them when people leave or a token might have been exposed.
Conclusion
Sanity gives you a lot of freedom, and best practices are what keep that freedom from turning into a mess. Model content by meaning and validate it. Write lean, typed, parameterised queries. Choose between the CDN and the live API deliberately, and cache in your framework with webhook revalidation. Keep tokens on the server, separate environments with datasets, and treat schema changes as migrations backed by real exports.
Then look after the people side: deploy the Studio from CI, give editors descriptions, previews and Visual Editing, and keep ownership and access clear. Use the launch checklist above before go-live, and when something does go wrong, the common Sanity mistakes guide is a good place to start looking.


