Type something to search...
Sanity Project Structure Explained for Beginners

Sanity Project Structure Explained for Beginners

A Sanity project structure is small by design. A new Studio contains sanity.config.ts, which configures the Studio itself (project ID, dataset, plugins and schema types); sanity.cli.ts, which tells the CLI which project and dataset to use for commands like deploy; a schemaTypes/ folder that exports your content types; a static/ folder for files bundled with the Studio; plus package.json, tsconfig.json and .gitignore. Everything else, including the Studio's interface, comes from npm packages, so you only maintain your configuration and custom code.

Knowing which file controls what saves a lot of confusion, particularly the difference between the two config files. This guide walks through each file and folder in a freshly created Studio, explains the most useful options in both config files, shows how environment variables and workspaces fit in, and suggests ways to organise a Studio as it grows, both standalone and alongside a Next.js app.

A Fresh Studio at a Glance

After running npm create sanity@latest with the clean template and TypeScript, you'll have this:

my-studio/
├── .sanity/              # generated at runtime, git-ignored
├── dist/                 # created by sanity build, git-ignored
├── node_modules/
├── schemaTypes/
│   └── index.ts
├── static/
│   └── .gitkeep
├── .gitignore
├── package.json
├── README.md
├── sanity.cli.ts
├── sanity.config.ts
└── tsconfig.json
PathPurposeEdit it?
sanity.config.tsStudio configurationYes, often
sanity.cli.tsCLI configurationOccasionally
schemaTypes/Content modelYes, constantly
static/Files bundled with the StudioRarely
package.jsonDependencies and scriptsWhen adding plugins
tsconfig.jsonTypeScript settingsRarely
.sanity/Temporary runtime filesNo
dist/Production build outputNo

If you haven't created a project yet, how to set up your first Sanity project walks through it.

sanity.config.ts: The Studio Configuration

This is the heart of the Studio. It's loaded in the browser and decides which project and dataset the Studio connects to, what tools appear, which schema types exist and how documents behave. A freshly generated file looks like this:

// sanity.config.ts
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schemaTypes'

export default defineConfig({
  name: 'default',
  title: 'My Studio',

  projectId: 'abc123xy',
  dataset: 'production',

  plugins: [structureTool(), visionTool()],

  schema: {
    types: schemaTypes,
  },
})

The main properties:

  • name: an internal identifier for the workspace. Required when you have several.
  • title: shown in the Studio's navigation bar and browser tab.
  • projectId and dataset: which Content Lake project and dataset to read and write.
  • plugins: tools and extensions. structureTool() gives you the main content browser and visionTool() the GROQ playground.
  • schema.types: the array of document and object types that make up your content model.

As your Studio grows, you'll add more:

export default defineConfig({
  // ...
  basePath: '/studio',
  icon: CogIcon,
  schema: {
    types: schemaTypes,
    templates: (prev) => prev.filter((t) => t.schemaType !== 'siteSettings'),
  },
  document: {
    actions: (prev, context) =>
      context.schemaType === 'siteSettings'
        ? prev.filter(({ action }) => action !== 'delete' && action !== 'duplicate')
        : prev,
    newDocumentOptions: (prev) => prev.filter((item) => item.templateId !== 'siteSettings'),
  },
})
  • basePath: the URL path the Studio lives under, needed when it's embedded or hosted at a sub-path.
  • schema.templates: controls initial value templates, here hiding a singleton from "create new" lists.
  • document.actions: adds, removes or replaces document actions, such as Publish or Delete.
  • document.newDocumentOptions: filters what appears in the global create menu.

Many properties accept a (prev, context) => ... callback like these, so you can adjust defaults per document type, per user or per workspace. The pattern above is the standard way to build singleton documents.

sanity.cli.ts: The CLI Configuration

This file is read only by the CLI in your terminal and CI, never by the browser. It tells commands like sanity deploy, sanity datasets export and sanity documents query which project to target:

// sanity.cli.ts
import { defineCliConfig } from 'sanity/cli'

export default defineCliConfig({
  api: {
    projectId: 'abc123xy',
    dataset: 'production',
  },
  deployment: {
    appId: 'your-studio-app-id',
    autoUpdates: true,
  },
})
  • api.projectId and api.dataset: the defaults for project-specific CLI commands.
  • deployment.appId: identifies a deployed Studio so later deploys update the same one. You'll find it under Studios in your project at sanity.io/manage after the first deploy.
  • deployment.autoUpdates: when true, the deployed Studio loads the latest compatible Sanity version at runtime, so you get fixes without redeploying.

Other options include server (the dev server's hostname and port), project.basePath for Studios served under a sub-path, graphql for GraphQL API deployment and vite for customising the bundler.

Why Are There Two Config Files?

It's a common question. They serve different audiences:

sanity.config.tssanity.cli.ts
Read byThe Studio, in the browserThe CLI, in the terminal
ControlsSchema, plugins, tools, document behaviourDefault project, deployment, dev server, build
Can defineMultiple workspacesOne default project and dataset
Needed when embedding in Next.jsYesOnly for CLI commands

Keeping projectId and dataset in sync between them matters. If they differ, the CLI might deploy or export a different dataset from the one the Studio edits. Using environment variables in both, as shown below, avoids that. The CLI commands guide shows what each command uses from sanity.cli.ts.

The schemaTypes/ Folder

This folder holds your content model. The clean template starts with one file:

// schemaTypes/index.ts
export const schemaTypes = []

As you add types, create a file for each and register it in the array:

schemaTypes/
├── index.ts
├── documents/
│   ├── postType.ts
│   ├── authorType.ts
│   └── categoryType.ts
└── objects/
    ├── seoType.ts
    └── blockContentType.ts
// schemaTypes/index.ts
import { postType } from './documents/postType'
import { authorType } from './documents/authorType'
import { categoryType } from './documents/categoryType'
import { seoType } from './objects/seoType'
import { blockContentType } from './objects/blockContentType'

export const schemaTypes = [postType, authorType, categoryType, seoType, blockContentType]

Splitting documents (things editors create and list, like posts) from objects (reusable field groups, like SEO metadata) keeps the folder easy to scan. The folder name is only a convention: older projects and some templates call it schemas/, and Sanity doesn't care as long as sanity.config.ts imports the array. For writing the types themselves, see your first Sanity schema and reusable object types.

The static/ Folder

Files in static/ are copied into the build and served from the Studio's /static/ path. Use it for things like a custom logo image or a favicon file referenced by your Studio. Most projects leave it empty, which is why the template includes a .gitkeep file to keep the folder in Git.

package.json, tsconfig.json and .gitignore

The generated package.json includes:

  • Dependencies: sanity (the Studio and CLI), @sanity/vision, react, react-dom and styled-components.
  • Dev dependencies: TypeScript, ESLint with @sanity/eslint-config-studio, Prettier and React types.
  • Scripts: dev, build, deploy, deploy-graphql and start, which call the matching CLI commands.

tsconfig.json holds TypeScript settings for the Studio, which is compiled with Vite. .gitignore already excludes node_modules, dist, .sanity, logs and *.local env files.

The .sanity/ and dist/ Folders

  • .sanity/ is a temporary folder the CLI regenerates every time the dev server starts. Never edit or commit it.
  • dist/ is created by sanity build and contains the static production Studio: an index.html and bundled assets. You'd upload this to your own host if you self-host. sanity deploy builds and uploads it for you, as covered in how to deploy Sanity Studio.

Environment Variables

Rather than hard-coding the project ID and dataset, read them from environment variables. Variables used by the Studio must start with SANITY_STUDIO_:

# .env
SANITY_STUDIO_PROJECT_ID=abc123xy
SANITY_STUDIO_DATASET=production
// sanity.config.ts
export default defineConfig({
  projectId: process.env.SANITY_STUDIO_PROJECT_ID!,
  dataset: process.env.SANITY_STUDIO_DATASET!,
  // ...
})
// sanity.cli.ts
export default defineCliConfig({
  api: {
    projectId: process.env.SANITY_STUDIO_PROJECT_ID,
    dataset: process.env.SANITY_STUDIO_DATASET,
  },
})

The Studio loads .env, .env.local, .env.[mode] and .env.[mode].local automatically. build and deploy run in production mode, so they load .env.production. Set SANITY_ACTIVE_ENV=staging to load .env.staging instead.

Two cautions:

  • These values end up in the browser bundle. The prefix exists precisely so that only intended values are exposed. Never put API tokens or secrets in SANITY_STUDIO_ variables.
  • Use the full name. Values are replaced statically at build time, so process.env.SANITY_STUDIO_DATASET works, but process.env[name] doesn't.

Multiple Workspaces

Pass an array to defineConfig to run several workspaces in one Studio, for example production and staging datasets with the same schema:

// 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' },
])

Each workspace needs a unique name and basePath. A switcher appears next to the Studio title. The hidden option can hide a workspace from some users, but it only affects the interface, not data access.

Organising a Growing Studio

Sanity doesn't enforce a folder layout. For Studios with more than a handful of customisations, the recommended pattern is a src/ folder grouped by customisation type, with custom components kept next to the schema they belong to:

my-studio/
├── sanity.config.ts
├── sanity.cli.ts
├── src/
│   ├── schemaTypes/
│   │   ├── index.ts
│   │   ├── postType.ts
│   │   └── seoType/
│   │       ├── index.ts
│   │       └── SeoInput.tsx
│   ├── structure/
│   │   └── index.ts
│   ├── actions/
│   ├── badges/
│   └── plugins/
└── static/
  • structure/: your Structure Builder configuration, passed to structureTool({ structure }).
  • actions/ and badges/: custom document actions and status badges.
  • plugins/: local plugins that bundle several customisations together.

Co-locating a custom input like SeoInput.tsx with seoType makes it easy to find later, and easy to extract into a plugin if you want to share it across Studios.

Sanity Inside a Next.js Project

There are two common ways to combine a Studio with a website.

Separate Folders (Monorepo)

my-site/
├── studio/        # standalone Sanity Studio
│   ├── sanity.config.ts
│   ├── sanity.cli.ts
│   └── schemaTypes/
└── web/           # Next.js front end
    ├── src/app/
    └── src/sanity/
        ├── client.ts
        └── queries.ts

Each app has its own package.json and deploys separately. The Studio goes to *.sanity.studio, the site to your host. This is simple to reason about and lets you upgrade each independently.

Embedded Studio

my-site/
├── sanity.config.ts
├── sanity.cli.ts
├── src/
│   ├── app/
│   │   └── studio/[[...tool]]/page.tsx
│   └── sanity/
│       ├── schemaTypes/
│       ├── lib/client.ts
│       └── env.ts
└── package.json

When you run sanity init inside a Next.js app, the CLI can generate this layout, mounting the Studio at /studio with a catch-all route and keeping schemas under src/sanity/. One deployment serves both site and Studio. Environment variables in this setup typically use the NEXT_PUBLIC_SANITY_ prefix so Next.js can read them.


FAQ: Sanity Project Structure

sanity.config.ts configures the Studio that runs in the browser, including schema types, plugins and document behaviour. sanity.cli.ts configures the command-line tool, including the default project and dataset for commands and deployment settings.

No. It's a convention used by current templates. You can name it schemas or anything else, as long as sanity.config.ts imports the array of types and passes it to schema.types.

No. The .sanity folder is regenerated whenever the dev server starts, and dist is the output of sanity build. Both are listed in the generated .gitignore.

The prefix tells the build which variables are safe to include in the browser bundle, so unrelated variables on your machine aren't exposed. Because these values are public, never use them for tokens or secrets.

Yes. Pass an array of workspace configurations to defineConfig, each with its own name, basePath and dataset. Editors switch between them from a menu next to the Studio title.

Both work. Embedding gives you a single deployment and URL. A separate Studio folder keeps dependencies independent, works with Studio auto-updates and makes upgrades simpler. Many teams use a monorepo with separate studio and web folders.


Conclusion

A Sanity Studio project is mostly configuration. sanity.config.ts defines what the Studio does, sanity.cli.ts tells the CLI where to point, schemaTypes/ holds your content model and the remaining files are standard JavaScript tooling. The Studio's interface itself lives in npm packages, which is why upgrades are usually painless.

Start with the generated layout, move project IDs and datasets into SANITY_STUDIO_ environment variables, and introduce a src/ folder with schema, structure and action subfolders once customisations pile up. Whether you keep the Studio separate or embed it in Next.js, the same files and ideas apply.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper