
Understanding Sanity Datasets and Environments
A Sanity dataset is a self-contained collection of JSON documents inside a project, and an environment is a dataset plus the Studio and applications configured to read from it. A typical setup has a production dataset that editors work in and a development or staging dataset for testing schema changes, migrations and new code against realistic content. You switch environments by changing the dataset value in your Studio config and client config, usually from an environment variable. For everyday content changes you don't need a second dataset at all: drafts and releases already keep unpublished work out of production.
Getting environments right early saves a lot of pain later. Too few and you end up testing a risky migration on live content. Too many and editors work in the wrong place while content drifts between copies. This guide explains how datasets work, when a separate dataset earns its keep, how schema and content flow between environments, how to configure the Studio and front end per environment, and the plan limits and gotchas to plan around.
What a Dataset Is
Every Sanity project has an ID and one or more datasets. Think of the project as the account, billing and member list, and each dataset as a separate database:
- Documents live in exactly one dataset: GROQ queries and normal references work within a dataset, not across them.
- Users and roles belong to the project: the same people can access every dataset unless you restrict them with roles.
- Assets are per dataset: an image uploaded to
stagingisn't available inproductionunless you copy it. - History is per dataset: each dataset keeps its own transaction log.
Dataset names can be 1 to 64 characters long, may only contain lowercase letters, numbers, hyphens and underscores, and must start and end with a letter or number. The Content Lake explained post covers how documents, drafts and perspectives work inside a dataset.
Public and Private Datasets
| Visibility | Who can read published documents | Plan |
|---|---|---|
| Public | Anyone, without a token | All plans |
| Private | Only requests with a valid token | Growth and above |
Drafts and release versions are private in both cases. One detail worth remembering: an unauthenticated query to a private dataset doesn't return an error, it returns an empty result such as []. If a staging site suddenly shows nothing, check the token before assuming the content is gone.
Do You Need More Than One Dataset?
Sanity's own recommendation has shifted towards doing most content work in a single production dataset. Unpublished content already lives safely beside published content:
- Drafts hold unpublished edits to a single document.
- Scheduled drafts publish a single document at a set time (Growth and above).
- Content Releases group changes across many documents and publish them together (Enterprise).
Your production site queries with the published perspective and never sees any of it. Editors review changes in the real front end with live preview. So a staging dataset for "content that isn't ready yet" is usually the wrong tool.
Separate datasets do make sense when the change could break something:
| Change | Separate dataset? | Why |
|---|---|---|
| Editing or adding content | No | Drafts and releases handle it |
| Adding a new field or document type | Usually no | Additive changes don't affect existing content |
| Renaming, removing or restructuring fields | Yes | Existing documents must be migrated |
| Running a content migration script | Yes, first | Test it before touching production |
| New front-end code that needs realistic data | Yes | Developers can break things freely |
| Webhooks or functions that write content | Yes | Avoid loops and stray writes in production |
| Separate sites or regions with different content | Maybe | Datasets give hard separation; see multi-site setups |
A common, sensible setup is production plus development. Add staging only if you have a real staging deployment with its own review step.
Schema Moves Up, Content Moves Down
The most useful mental model is that schema and content flow in opposite directions:
- Schema and code move up: schema types, validation, Studio config and front-end queries are files in Git. You promote them like any code, from development to staging to production.
- Content moves down: you copy production content into development and staging so you test against real documents.
- Editors author in production: nobody writes "real" content in staging and promotes it.
This works because the Content Lake doesn't enforce your schema. A Studio with a newer schema can open documents written under an older one, and fields it doesn't know about simply stay in the data. Moving content up from development is the rare exception, such as seeding settings documents for a new feature, and it should be a one-off import rather than a sync. The mechanics of copying in both directions are in how to migrate content between Sanity datasets.
Creating and Managing Datasets
Use the CLI from your Studio folder:
npx sanity datasets list
npx sanity datasets create development --visibility public
npx sanity datasets visibility set staging private
npx sanity datasets delete old-experiment
You can do the same in sanity.io/manage under Datasets. Deleting a dataset deletes its documents, assets and history, so export it first if there's any doubt. The CLI commands guide lists the other dataset subcommands.
Some features create add-on datasets automatically. Comments and Tasks, for example, store their data in a paired dataset next to yours. These don't count toward your plan's dataset limit, and you shouldn't point a Studio or site at them directly.
Configuring the Studio per Environment
There are two good patterns. Pick one and use it consistently.
Option 1: One Dataset per Deployment, via Environment Variables
Read the dataset name from an environment variable so each Studio deployment targets one dataset:
// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schemaTypes'
const dataset = process.env.SANITY_STUDIO_DATASET ?? 'development'
export default defineConfig({
name: 'default',
title: dataset === 'production' ? 'My Site' : `My Site (${dataset})`,
projectId: process.env.SANITY_STUDIO_PROJECT_ID ?? 'abc123xy',
dataset,
plugins: [structureTool(), visionTool()],
schema: { types: schemaTypes },
})
The Studio loads .env files by mode. sanity dev runs in development mode and sanity build and sanity deploy run in production mode, and you can pick another mode with SANITY_ACTIVE_ENV:
# .env.development
SANITY_STUDIO_DATASET=development
# .env.production
SANITY_STUDIO_DATASET=production
# .env.staging, used with SANITY_ACTIVE_ENV=staging npx sanity build
SANITY_STUDIO_DATASET=staging
Two rules apply. Variables must start with SANITY_STUDIO_ to reach the browser, and they're replaced at build time, so always reference them by their full name and never put secrets in them. Putting the dataset name in the Studio title is a cheap way to stop editors working in the wrong place.
Also set the dataset for CLI commands in sanity.cli.ts, since commands like datasets export and migrations run read it from there:
// sanity.cli.ts
import { defineCliConfig } from 'sanity/cli'
export default defineCliConfig({
api: {
projectId: process.env.SANITY_STUDIO_PROJECT_ID ?? 'abc123xy',
dataset: process.env.SANITY_STUDIO_DATASET ?? 'development',
},
})
Option 2: One Studio With a Workspace per Dataset
Pass an array to defineConfig and the Studio shows a workspace switcher:
// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { schemaTypes } from './schemaTypes'
const shared = {
projectId: 'abc123xy',
plugins: [structureTool()],
schema: { types: schemaTypes },
}
export default defineConfig([
{
...shared,
name: 'production',
title: 'Production',
basePath: '/production',
dataset: 'production',
},
{
...shared,
name: 'staging',
title: 'Staging',
basePath: '/staging',
dataset: 'staging',
hidden: ({ currentUser }) =>
!currentUser?.roles.some((role) => role.name === 'administrator'),
},
])
Workspaces suit teams where developers and editors share one deployed Studio. The hidden option only hides the workspace in the UI; it doesn't stop anyone reading the dataset, so use roles for real access control. If staging needs a different schema from production, Option 1 is cleaner, because each deployment ships its own code.
Deploy Schemas per Dataset
Deployed schemas, which some Sanity platform features read instead of your local code, are stored per dataset. npx sanity schemas deploy deploys each workspace's schema to that workspace's dataset, so run it again in each environment after you change the schema.
Configuring the Front End per Environment
Your site's client needs the same switch. In Next.js:
// src/sanity/env.ts
export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET ?? 'development'
export const apiVersion = '2026-10-01'
// src/sanity/lib/client.ts
import { createClient } from 'next-sanity'
import { apiVersion, dataset, projectId } from '../env'
export const client = createClient({ projectId, dataset, apiVersion, useCdn: true })
Then set NEXT_PUBLIC_SANITY_DATASET per environment in your host: production for the production deployment and development or staging for preview deployments. The dataset name isn't a secret, so a public variable is fine. Tokens are, so keep them in server-only variables. The full client setup is in Sanity with the Next.js App Router.
Project Resources Aren't Per Dataset
A dataset holds content, but an environment also depends on project-level settings. Check each of these when you add one:
- CORS origins: add the staging Studio and site URLs, or browser requests fail.
- Webhooks: each webhook targets a dataset (or all of them with
*). A webhook that triggers production rebuilds shouldn't fire for staging edits. See Sanity webhooks. - API tokens: tokens are project-wide by default. Use separate tokens per environment so you can revoke one without breaking the others.
- Roles: restricting a role to specific datasets relies on custom roles, which are an Enterprise feature. With the predefined roles, a member's role applies to every dataset in the project.
Phasing In Schema Changes Without a Second Dataset
For additive changes, you can ship to production and hide the new field until the front end is ready. Read a flag from a Studio environment variable and skip validation while it's hidden:
// schemaTypes/fields/subtitle.ts
import { defineField } from 'sanity'
const showNewFields = process.env.SANITY_STUDIO_SHOW_NEW_FIELDS === 'true'
export const subtitle = defineField({
name: 'subtitle',
title: 'Subtitle',
type: 'string',
hidden: !showNewFields,
validation: (rule, context) => (context?.hidden ? rule.skip() : rule.required()),
})
Set the flag to true in development, leave it unset in production, and redeploy the Studio when you're ready to expose the field. Hiding only affects the form: values already stored still appear in query results. Conditional fields covers hidden in more depth.
Plan Limits and Costs
| Plan | Datasets | Private datasets |
|---|---|---|
| Free | 2 | No |
| Growth | 2, with extra datasets as a paid add-on | Yes |
| Enterprise | Custom | Yes |
Two datasets covers production and development on any plan, which is enough for most projects. Copying a dataset in the cloud and dataset aliases (hot swapping) are part of Advanced Dataset Management on Enterprise plans. Export and import work everywhere. Current prices are in the Sanity pricing guide.
Common Mistakes
- Authoring real content in staging: it has to be moved later and will conflict with production edits. Author in production with drafts.
- Letting development drift: refresh it from production regularly so tests reflect real content.
- Hard-coding the dataset name: one forgotten file and a script writes to the wrong place. Read it from configuration everywhere.
- Forgetting the CLI config:
sanity.cli.tsdecides wheremigrations runanddatasets importwrite by default. - Assuming references work across datasets: they don't by default. Cross-dataset references are a separate feature with their own setup.
FAQ: Sanity Datasets and Environments
The Free plan includes two datasets, and both must be public. That is usually enough for a production dataset and a development dataset. Growth also includes two, with private visibility and the option to pay for extra datasets.
Usually not. Drafts, scheduled drafts and Content Releases keep unpublished work out of your live site while it stays in the production dataset. Use separate datasets for risky schema changes, migrations and development.
Not with normal queries or references, which only work within a single dataset. Sanity offers cross-dataset references as a separate feature, but most projects are simpler if related content lives in the same dataset.
Either read the dataset from a SANITY_STUDIO_ environment variable and deploy one Studio per environment, or define several workspaces in sanity.config.ts with one dataset each. The workspace approach adds a switcher to the Studio navbar.
No. Members and their roles belong to the project and apply to every dataset by default. Restricting a role to particular datasets requires custom roles, which are available on Enterprise plans.
No. Images and files are stored per dataset. When you copy content into another dataset, include the assets in the export, or the image references in the copied documents won't resolve.
Conclusion
A Sanity environment is simply a dataset plus the Studio and apps pointed at it. Keep editors in production, where drafts and releases protect unpublished work, and use a development or staging dataset for the changes that can genuinely break things: restructured schemas, migrations, new code and integrations that write content.
Configure every piece from the same environment variables, label non-production Studios clearly, and remember that CORS origins, webhooks, tokens and deployed schemas need attention per environment too. With schema moving up through Git and content copied down from production, your environments stay predictable and your production content stays safe.


