# Course: Architecture & DevOps
https://www.sanity.io/learn/course/architecture-and-devops

Whether you're first getting your project off the ground or developing new features in an existing one, having a well-defined end-to-end development workflow is crucial for shipping your work efficiently and reliably without impacting the day-to-day content operations of your editors.

---

## Navigation

## Contents

1. [Introduction to Development Workflow](https://www.sanity.io/learn/course/architecture-and-devops/introduction-to-development-workflow) · [markdown](https://www.sanity.io/learn/course/architecture-and-devops/introduction-to-development-workflow.md)
2. [Setting Up Your Environments](https://www.sanity.io/learn/course/architecture-and-devops/setting-up-your-environments) · [markdown](https://www.sanity.io/learn/course/architecture-and-devops/setting-up-your-environments.md)
3. [Deploying Environment-Specific Studios](https://www.sanity.io/learn/course/architecture-and-devops/deploying-environment-specific-studios) · [markdown](https://www.sanity.io/learn/course/architecture-and-devops/deploying-environment-specific-studios.md)
4. [Automating Development Workflow](https://www.sanity.io/learn/course/architecture-and-devops/automating-development-workflow) · [markdown](https://www.sanity.io/learn/course/architecture-and-devops/automating-development-workflow.md)

---

## Lesson 1: Introduction to Development Workflow
https://www.sanity.io/learn/course/architecture-and-devops/introduction-to-development-workflow

Sanity’s code-first approach makes it uniquely suited for automation and naturally aligns with CI/CD to supports safe, continuous iteration without disrupting content teams.

## Sanity's Mental Model



The Studio, from your schema definitions and configuration, defines the structure of your content, enforces the rules and validation of that content, and allows you to customize the editorial interface for how your editors interact with and manage that content. You can think of the Studio as a customizable "window" through which your editors interact with Content Lake. Sanity provides a unique architecture that decouples the content editing experience in the Studio from the underlying content storage in the Content Lake. 



![Image](https://cdn.sanity.io/images/3do82whm/next/83ef968df598f301495a0a6341e5fd0b6da20be5-12288x4754.png)

One of the key benefits of this architecture is that the Studio's schema and configuration live entirely in code, which means you can manage them in source control and test any changes as part of your regular development and QA process.



In other words, a solid development workflow allows you to:



- Develop and test new features in an isolated environment separate from production

- Promote changes from development to production environments in a controlled manner

- Allow content editors to continue their work uninterrupted in the production environment while development is ongoing


By setting up separate development and production environments, along with processes to migrate code and content between them, you can establish a smooth flow from development to release. This ensures that new features are properly tested before reaching production, and that content editors always have a stable production environment to work in that is insulated from development activities.



## What is DevOps?



According to [Atlassian](https://www.atlassian.com/devops),



> DevOps is a set of [practices](https://www.atlassian.com/devops/what-is-devops/devops-best-practices), [tools](https://www.atlassian.com/devops/devops-tools/choose-devops-tools), and a [cultural philosophy](https://www.atlassian.com/devops/what-is-devops/devops-culture) that automate and integrate the processes between software development and IT teams. It emphasizes team empowerment, cross-team communication and collaboration, and technology automation.



Continuous integration and continuous delivery (CI/CD) automates the development workflow above and allows developers to iterate quickly, catch issues early, and deliver new features seamlessly. Integrating the development and content workflows through CI/CD empowers developers and content editors to collaborate effectively on delivering new experiences. Developers can focus on building and shipping features, while content editors can create and manage content without disruption. This setup provides a robust foundation for ongoing development and content operations to occur in parallel–all in service of enabling your organization to realize its business goals.



In the upcoming lessons, we'll walk through the specific steps to configure your Sanity project with multiple environments and datasets to support this development workflow. You'll learn how to structure your project, manage datasets, and deploy Studios. By the end, you'll have a the foundations of a setup to confidently develop and ship ongoing improvements to your project.



---

## Lesson 2: Setting Up Your Environments
https://www.sanity.io/learn/course/architecture-and-devops/setting-up-your-environments

Separate development and production environments ensure isolated testing, stable workflows, and safe content migrations without disrupting editors.

> [!TIP]
> This course assumes you have already initialized a Sanity Studio as described in [Day one content operations](https://www.sanity.io/learn/course/day-one-with-sanity-studio).



## Create Development Dataset



When developing new features, the code changes made by developers to schemas and other Studio configuration shouldn't impact content editors  working in the production environment. That's why it's a best practice to have separate datasets and Studio deployments for development and production environments.



By provisioning a dedicated development dataset, developers can freely iterate and test code changes without worrying about interrupting the day-to-day content operations. This clean separation allows both content and development workflows to proceed in parallel, while keeping the production environment stable. As new features are validated in the development environment, the code changes can be promoted to production, and any necessary content migrations can be performed in a controlled manner. Developing in a separate environment can also ensure that any schema changes they make will have migration scripts. Meanwhile, content editors can continue their work in the production dataset, insulated from any development activities.



First, to complement the `production` dataset you should already have, create a `development` dataset using the CLI:



```sh
npx sanity dataset create development --visibility private
```

You should now see two datasets when you run `npx sanity dataset list` and in Manage (`npx sanity manage)`.



## Using Environment Variables



Environment variables allow your Studio configuration to adapt to each deployment without modifying the codebase—making them essential for managing different environments. Rather than hardcoding the project ID and dataset, for example, you can instead use environment variables to statically replace them at build time.



First, initialize a new environment file by running:



```sh
npx sanity init --env --project [your-project-id] --dataset production
```

You'll see a new `.env` file in your workspace:



```:.env
# Warning: Do not add secrets (API keys and similar) to this file, as it is source controlled!
# Use `.env.local` for any secrets, and ensure it is not added to source control

SANITY_STUDIO_PROJECT_ID="[your-project-id]"
SANITY_STUDIO_DATASET="production"
```

> [!NOTE]
> `.env` by default won't be ignored by Git; however, these two environment variables aren't considered sensitive. If you'd rather they weren't checked into source control, you can use `--env .env.local`.



Now duplicate `.env` and name it `.env.development`, and remove the project ID so you just have the following:



```:.env.development
# Warning: Do not add secrets (API keys and similar) to this file, as it is source controlled!
# Use `.env.local` for any secrets, and ensure it is not added to source control

SANITY_STUDIO_DATASET="development"
```

> [!NOTE]
> In this example, we'll be checking these into source control. If you needed to override a variable on your local machine, you could add a `.env[.mode].local` file with your override(s).



Finally, let's update our configuration files to read from environment variables:



```typescript:sanity.config.ts
import {defineConfig} from 'sanity'

export default defineConfig({
  // ...
  
  projectId: process.env.SANITY_STUDIO_PROJECT_ID!,
  dataset: process.env.SANITY_STUDIO_DATASET!,
  
  // ...
})
```

```typescript:sanity.cli.ts
import {defineCliConfig} from 'sanity/cli'

export default defineCliConfig({
  api: {
    projectId: process.env.SANITY_STUDIO_PROJECT_ID!,
    dataset: process.env.SANITY_STUDIO_DATASET!,
  },
  
  // ...
})
```

## Conclusion



In this lesson, we covered how to set up separate development and production datasets in your Sanity project. By creating dedicated datasets and configuring environment variables, you can establish a clean separation between ongoing development work and the stable production environment used by content editors.



With this foundation in place, you're ready to deploy separate Sanity Studios for each environment. In the next lesson, we'll walk through the process of deploying a development Studio and how to set CORS origins for each environment.



---

## Lesson 3: Deploying Environment-Specific Studios
https://www.sanity.io/learn/course/architecture-and-devops/deploying-environment-specific-studios

Deploying separate Studios ensures clean environment separation, safer iteration, and uninterrupted content editing.

With our datasets and environment files in place, let's now walk through the process of deploying separate Sanity Studios for each environment and setting up the proper CORS origins.



## Designate a Studio Host



First, we'll need to configure the subdomain for our Studio deployments. Sanity CLI will either use the `studioHost` option in `sanity.cli.ts`, if it's provided, or prompt for a hostname in the terminal.



Like our project ID and dataset, we can use an environment variable to configure the hostname for our Studio deployment. So let's add a new environment variable, `SANITY_STUDIO_HOSTNAME`, to our `.env` file:



```:.env
# Warning: Do not add secrets (API keys and similar) to this file, as it source is controlled!
# Use `.env.local` for any secrets, and ensure it is not added to source control

SANITY_STUDIO_PROJECT_ID="[your-project-id]"
SANITY_STUDIO_DATASET="development"

# [hostname].sanity.studio
HOSTNAME="[your-hostname]"
SANITY_STUDIO_HOSTNAME="$HOSTNAME"
```

Then in your `.env.development` file add:



```:.env.development
# https://www.sanity.io/docs/environment-variables
# Warning: Do not add secrets (API keys and similar) to this file, as it is source controlled!
# Use `.env.local` for any secrets, and ensure it is not added to source control

SANITY_STUDIO_DATASET="development"

# [hostname]-development.sanity.studio
SANITY_STUDIO_HOSTNAME="${HOSTNAME}-development"
```

> [!NOTE]
> Here we're using a variable `HOSTNAME` as the base and then using the `dotenv-expand` syntax to reference and add a suffix.



Now let's add a `studioHost` option and set it to the value of our new environment variable:



```typescript:sanity.cli.ts
import {defineCliConfig} from 'sanity/cli'

export default defineCliConfig({
  api: {
    projectId: process.env.SANITY_STUDIO_PROJECT_ID!,
    dataset: process.env.SANITY_STUDIO_DATASET!,
  },

  studioHost: process.env.SANITY_STUDIO_HOSTNAME!,

  // ...
})
```

## Targeting Environments with Modes



Now that we've setup our environment variables and configured our CLI and Studio configuration files to read from them, we need a way to target a specific environment.



Sanity CLI will load your environment variables in a predictable order, which we have leveraged to set environment variables for our different modes (`production` vs. `development`). `.env` will be loaded in all modes, so we'll use it as our fallback and add `development`-specific overrides. For example, we've overridden `SANITY_STUDIO_DATASET` and we've suffixed `HOSTNAME` to set `SANITY_STUDIO_HOSTNAME`.



When running Sanity CLI, you can specify the intended mode for your commands. Commands like `build` and `deploy` will run in `production` mode by default. To target a different environment, you can set the mode by specifying `SANITY_ACTIVE_ENV` in your terminal:



```sh
# builds Sanity Studio in `development` mode, loading `.env` and then `.env.development`

SANITY_ACTIVE_ENV=development npm run build
```

> [!TIP]
> You can learn more about modes and environment variable loading order in [Environment Variables](https://www.sanity.io/learn/studio/environment-variables)



Now let's deploy our two Studio environments:



```sh
# Deploy the development Studio
SANITY_ACTIVE_ENV=development npm run deploy

# Deploy the production Studio
npm run deploy
```

The last step will be to add your Studio environments to your CORS origin. Navigate to the Studio URL's that you've just created. If they haven't yet been added as CORS origins, you'll be prompted to add them to Manage. You can also either run `npx sanity manage` from your terminal or open Manage in your browser directly. Navigate to the 'API' tab and add your Studio URL's as origins with credentials allowed.



## Conclusion



Congratulations—you now have separate, environment-specific Studios configured and deployed! This setup gives your team the freedom to iterate safely in development while keeping production stable for content editors. Up until now, you've been running CLI commands manually, carefully passing the right environment mode. With everything now structured and standardized, you're ready to take the next step: automating your deployment. In the next lesson, we’ll connect these pieces into a CI/CD pipeline that streamlines your workflow and eliminates manual steps.



---

## Lesson 4: Automating Development Workflow
https://www.sanity.io/learn/course/architecture-and-devops/automating-development-workflow

Automate Sanity Studio deployments and CI checks that validate schemas and content, ensuring every code change is rigorously reviewed and production-ready.

Now that your environments and Studios are fully configured, it’s time to automate the workflow.



In this lesson, we will explore how automating the deployment of your Sanity Studio streamlines your development process and helps you achieve faster, more reliable releases. By transitioning from manual deployments to an automated workflow, you not only ensure that your production code is built and deployed consistently, but you also gain immediate feedback on changes with minimal human intervention.



## Development Workflow



![Flowchart showing the process of authoring a feature into production](https://cdn.sanity.io/images/3do82whm/next/02f5e9c440d506e7b23f11b9414a4029390f29ba-1564x5222.png)

When developing new features, for example adding a new schema definition or creating a custom input component, should follow a consistent process:



1. Start by checking out a new feature branch

2. Making code changes while running the Studio locally

3. Once they're ready to deploy and looking for a code review, the developer will push their branch to the remote and open a pull request

4. Once their code has been reviewed and validated, they'll merge their pull request to the main branch.


## Automate Deployment



Imagine you have just committed changes to a feature branch and opened a pull request. Instead of manually building and deploying your Sanity Studio, like we did in the previous lesson, an automated process springs into action. The workflow is triggered by push or pull request events. First, it checks out the latest code from your branch, sets up the Node.js environment, and installs the dependencies. It'll then build your Studio and deploy it to a PR-numbered hostname. As an added benefit, the workflow also automatically posts a comment with a link to the preview environment where reviewers can see your changes. When your code is merged into the main branch, the workflow builds and deploys the Studio to the production environment. Once a pull request is closed, a separate job is triggered to clean up the associated preview deployment.



Here is a sample GitHub workflow that demonstrates this automated deployment process for a Sanity Studio.



> [!NOTE]
> Though written here for GitHub, these steps can be ported to any CI/CD provider and can be adapted to your preferred solution.



```yaml:deploy.yml
name: Deploy Sanity Studio

on:
  push:
    branches:
      - main
      - development
  pull_request:
    types: [opened, synchronize, reopened, closed]

permissions:
  contents: read
  pull-requests: write

env:
  SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
  cancel-in-progress: true

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')

    environment:
      name: ${{ github.ref == 'refs/heads/main' && 'Production' || github.ref == 'refs/heads/development' && 'Development' || 'Preview' }}
      url: ${{ steps.deploy.outputs.STUDIO_URL }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci

      - name: Set Studio hostname
        run: |
          if [ "${{ github.event_name }}" == "pull_request" ]; then
            echo "SANITY_STUDIO_HOSTNAME=${HOSTNAME}-pr-${{ github.event.pull_request.number }}" >> $GITHUB_ENV
          else
            echo "SANITY_STUDIO_HOSTNAME=${HOSTNAME}" >> $GITHUB_ENV
          fi

      - name: Build and deploy Sanity Studio
        id: deploy
        run: |
          if [ -z "${SANITY_STUDIO_HOSTNAME}" ]; then
            echo "Error: SANITY_STUDIO_HOSTNAME is not set" >&2
            exit 1
          fi

          if [[ "$SANITY_ACTIVE_ENV" == "development" ]]; then
            npm run deploy -- --yes --source-maps
          else
            npm run deploy -- --yes
          fi

          echo "STUDIO_URL=https://${SANITY_STUDIO_HOSTNAME}.sanity.studio" >> $GITHUB_OUTPUT

      - name: Post preview link
        if: github.event_name == 'pull_request' && github.event.action == 'opened'
        uses: actions/github-script@v7
        with:
          script: |
            const body = [
              '**🚀 Preview environment has been deployed!**',
              `Visit [${process.env.STUDIO_URL}](${process.env.STUDIO_URL}) to see your changes.`,
              "*This is a temporary environment that will be undeployed when this PR is merged or closed.*"
            ].join('\n\n')

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body,
            })
        env:
          STUDIO_URL: ${{ steps.deploy.outputs.STUDIO_URL }}

  teardown:
    name: Teardown
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request' && github.event.action == 'closed'

    environment:
      name: Preview

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: lts/*
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Cleanup PR preview
        run: npx sanity undeploy -- --yes
        env:
          SANITY_STUDIO_HOSTNAME: ${HOSTNAME}-pr-${{ github.event.pull_request.number }}
```

## Adding Pull Request Checks



Now that your Sanity Studio is deployed automatically, it’s crucial that every change merged into the main branch has been thoroughly reviewed and validated. When a pull request is opened or updated, your CI pipeline not only runs the typical linting and type-checking jobs but also includes Sanity-specific checks to catch errors early. If any of these jobs fail, detailed reports are automatically posted to the pull request, providing instant feedback for your team. In this way, before any merge occurs, your code is guaranteed to have passed all the necessary automated checks.



Within your CI pipeline, the commands `sanity schema validate` and `sanity documents validate` play critical roles in ensuring that your code does not introduce breaking changes. These validation steps create a robust safety net that goes beyond simply automating deployments.



The command `sanity schema validate` is used to verify that your schema definitions are error-free. When you run this command, it checks your schema files for syntax errors, misconfigurations, or other issues that might cause runtime errors.



In contrast, the command `sanity documents validate` verifies that the content stored in your Sanity dataset conform to the constraints defined in your schema. This command inspects each document to ensure that required fields are present, data types match the expected formats, and any additional validation rules you have implemented are adhered to. This step is essential for maintaining data integrity, and any discrepancies—such as missing values or incorrect data formats—are flagged to prevent problematic changes from being merged into production.



> [!TIP]
> Changes to your content model often require migration scripts to ensure data integrity. You can learn more about migrating data in [Handling schema changes confidently](https://www.sanity.io/learn/course/handling-schema-changes-confidently).



```yaml:ci.yml
name: CI

on:
  pull_request:
    types: [opened, synchronize, reopened]
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

env:
  SCHEMA_VALIDATION_REPORT: schema-report.txt
  DATASET_VALIDATION_REPORT: dataset-report.txt

jobs:
  typecheck:
    name: Typecheck
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          cache: npm
          node-version: lts/*
      - run: npm ci

      - name: Typecheck
        run: npm run typecheck

  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          cache: npm
          node-version: lts/*
      - run: npm ci

      - name: Lint
        run: npm run lint -- --max-warnings 0

  validate-schema:
    name: Validate Studio schema
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          cache: npm
          node-version: lts/*
      - run: npm ci

      - name: Validate Studio schema
        id: validate
        run: |
          npx sanity schema validate >> ${{ env.SCHEMA_VALIDATION_REPORT }}
          exit_code=$?
          {
            echo "## Schema Validation Results"
            echo "\`\`\`"
            cat ${{ env.SCHEMA_VALIDATION_REPORT }}
            echo "\`\`\`"
          } >> $GITHUB_STEP_SUMMARY
          exit $exit_code

      - name: Post schema validation report
        uses: actions/github-script@v6
        if: failure() && steps.validate.outcome == 'failure'
        with:
          script: |
            const fs = require('fs');
            const report = fs.readFileSync('${{ env.SCHEMA_VALIDATION_REPORT }}', 'utf8');
            const body = [
                '### ❌ Schema validation failed',
                '',
                `\`\`\`${report}\`\`\``,
            ].join('\n');

            await github.rest.issues.createComment({
                issue_number: context.issue.number,
                owner: context.repo.owner,
                repo: context.repo.repo,
                body,
            });

  validate-dataset:
    name: Validate dataset
    runs-on: ubuntu-latest
    if: (github.event_name == 'pull_request' && github.base_ref == 'main') || (github.ref == 'refs/heads/main')

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          cache: npm
          node-version: lts/*
      - run: npm ci

      - name: Validate dataset
        id: validate
        run: |
          npx sanity documents validate --yes --level info >> ${{ env.DATASET_VALIDATION_REPORT }}
          exit_code=$?
          {
            echo "## Dataset Validation Results"
            echo "\`\`\`"
            cat ${{ env.DATASET_VALIDATION_REPORT }}
            echo "\`\`\`"
          } >> $GITHUB_STEP_SUMMARY
          exit $exit_code
        env:
          SANITY_ACTIVE_ENV: production
          SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
          # TODO: delete
          SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_PROJECT_ID }}

      - name: Post dataset validation report
        if: failure() && steps.validate.outcome == 'failure'
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs'); 
            const report = fs.readFileSync('${{ env.DATASET_VALIDATION_REPORT }}', 'utf8');
            const body = [
                '### ❌ Dataset validation failed',
                '',
                `\`\`\`${report}\`\`\``,
            ].join('\n');

            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body
            });
```

By incorporating these validation steps into your GitHub workflow, you ensure that changes undergo rigorous review before they trigger the automated deployment process. This CI process not only enhances the quality and reliability of your Sanity Studio but also builds confidence that both its structure and underlying data are sound when updates are pushed to production.



> [!TIP]
> Changes to your content model often require migration scripts to ensure data integrity. You can learn more about migrating data in [Handling schema changes confidently](https://www.sanity.io/learn/course/handling-schema-changes-confidently).



You'll now have a robust DevOps process that enables continuous development while maintaining a stable production environment for your content team. This approach balances the needs of both developers and content editors, ensuring smooth operations and reliable deployments.



---

## Related Resources

- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)
