Tevpro insights

Deploying Sanity Studio to Cloudflare Workers with GitHub Actions

A step-by-step Sanity Studio deployment to Cloudflare Workers: build static assets, configure Wrangler, gate GitHub Actions on CI, and verify the protected editor experience.

SanityNext.jsCloudflareInfrastructure

To deploy a self-hosted Sanity Studio on Cloudflare Workers with GitHub Actions, build Studio into static assets, configure a Worker to serve the dist directory, and deploy it with Wrangler only after CI passes. Check out the exact commit CI tested. Cloudflare serves the authoring app; Sanity still hosts the Content Lake and APIs. Here is the file-by-file path, followed by the release and access controls Tevpro uses.

How to deploy Sanity Studio to Cloudflare Workers with GitHub Actions

1. Prepare the project and deployment boundary. Put the Studio in its own workspace (studio/) with a build script of "sanity build". Configure its Sanity project ID and dataset. Create a separate Cloudflare Worker and set up the Studio hostname and Cloudflare Access policy before inviting editors. In GitHub, store the Cloudflare API token as a secret; provide the account ID and Studio build values through your chosen environment configuration. Give the token only the permissions required to deploy this Worker.

2. Build the Studio assets. From the repository root, run npm ci, then npm run build --workspace=studio. The Studio package build script runs sanity build and writes its static application to studio/dist. SANITY_STUDIO_PROJECT_ID and SANITY_STUDIO_DATASET are build inputs in this example. Set optional Studio integration values, such as a preview URL or plugin license, only if your configuration uses them. Never bundle a private API token into the browser build.

3. Serve the built assets. For a static-only Studio Worker, studio/worker.ts can forward requests to the ASSETS binding. This minimal example intentionally omits any private application routes; do not copy it over an existing Worker entry point that implements them.

studio/worker.ts (static-only example)
typescript
interface Env {
  ASSETS: {fetch(request: Request): Promise<Response>}
}

export default {
  fetch(request: Request, env: Env): Promise<Response> {
    return env.ASSETS.fetch(request)
  },
}

4. Configure Wrangler. Point the Worker at the entry point and the Studio build directory; single-page-application fallback lets client-side Studio routes load directly. This is the isolated Studio Worker configuration, not the public Next.js/OpenNext Worker. With workers_dev disabled, configure a custom hostname or route for the Worker separately, then protect that hostname with Cloudflare Access.

studio/wrangler.jsonc
json
{
  "$schema": "../node_modules/wrangler/config-schema.json",
  "name": "tevpro-sanity-studio",
  "main": "worker.ts",
  "compatibility_date": "2026-08-08",
  "workers_dev": false,
  "observability": {"enabled": true},
  "assets": {
    "directory": "dist",
    "binding": "ASSETS",
    "not_found_handling": "single-page-application"
  }
}

5. Gate and deploy with GitHub Actions. The workflow below listens for a successful main-branch CI run, checks out its head SHA, validates required configuration, builds the Studio, authenticates to Cloudflare without an interactive login, and deploys through Wrangler. workflow_dispatch is reserved for deliberate recovery. The CI workflow itself should type-check and build Studio before the deploy workflow starts.

6. Verify the release. Check the Studio hostname, open a nested Studio route, confirm authorized editors pass Cloudflare Access and Sanity sign-in, and test Presentation mode against the intended public-site environment. Confirm the deployed commit matches the CI-tested SHA; a successful Wrangler command alone does not prove the editor journey works.

The deployment split

This guide is for teams deciding whether to deploy Sanity Studio beside a public Next.js application or as a separate authoring surface. Tevpro keeps the Studio and public application as separate Workers so each can deploy independently while using the same Sanity project and dataset.

Sanity Studio deployment flow
Mermaid

Cloudflare hosts the Studio shell and its static assets. Sanity remains the content platform behind it: the Studio connects to the Sanity project, dataset, Content Lake, and APIs. Sanity documents the Studio configuration that connects the app to a project and dataset here.

Use narrow workflow triggers

The Studio deployment starts only after CI succeeds for a push to main. It checks out the exact commit CI evaluated, not whichever commit is current when the deployment job begins. An explicit manual dispatch remains available for a reviewed recovery release.

deploy-studio-worker.yml
yaml
name: Deploy Sanity Studio Worker
on:
  workflow_dispatch:
  workflow_run:
    workflows: [CI]
    types: [completed]
    branches: [main]

permissions:
  contents: read

concurrency:
  group: sanity-studio-production-${{ github.ref }}
  cancel-in-progress: true

env:
  NODE_VERSION: "22"
  DEPLOY_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
  CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID || secrets.CLOUDFLARE_ACCOUNT_ID }}
  SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
  SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
  SANITY_STUDIO_PREVIEW_URL: ${{ vars.SANITY_STUDIO_PREVIEW_URL }}

jobs:
  deploy:
    if: >-
      github.event_name == 'workflow_dispatch' ||
      (github.event.workflow_run.conclusion == 'success' &&
      github.event.workflow_run.event == 'push' &&
      github.event.workflow_run.head_branch == 'main')
    runs-on: [self-hosted, Linux, X64, tevpro]
    environment: production
    steps:
      - uses: actions/checkout@v6
        with:
          ref: ${{ env.DEPLOY_SHA }}
      - uses: nubjs/setup-nub@v0
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: false
      - run: npm ci --include=optional --prefer-offline --no-audit --fund=false
      - name: Validate required configuration
        run: |
          for name in CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID SANITY_STUDIO_PROJECT_ID SANITY_STUDIO_DATASET SANITY_STUDIO_PREVIEW_URL; do
            if [ -z "${!name}" ]; then
              echo "::error::Missing deployment variable: $name"
              exit 1
            fi
          done
      - run: npm run build --workspace=studio
      - working-directory: studio
        run: nubx wrangler whoami
      - working-directory: studio
        run: nubx wrangler deploy --message "Studio @ ${{ env.DEPLOY_SHA }}"

This is a trimmed production pattern, not a drop-in workflow. Tevpro’s full workflow also validates optional Studio integrations and handles separate private Worker secrets that are deliberately outside this static deployment example. The core control is DEPLOY_SHA: deploy the commit CI tested. Match runner labels, token permissions, package manager, and environment values to your own repository. If you do not use nub, run the installed Wrangler CLI through npx instead.

Use CI credentials, not interactive login

Our workflow passes a Cloudflare API token and account ID through GitHub environment configuration, then checks authentication before deployment. Cloudflare recommends an API token and account ID for non-interactive CI jobs. The token belongs in GitHub secrets. The account ID can be an environment variable or secret, depending on the team’s access model.

Protect the Studio with Cloudflare Access

We protect the self-hosted Studio with Cloudflare Access before it reaches Sanity. Authorized staff must complete the organization’s edge identity check, then Sanity applies its own project and dataset authorization.

The access policy and identity-provider configuration are operational controls, not part of the public implementation recipe. We keep those details in internal runbooks. The deployment workflow receives only the credentials needed to publish the Worker. It does not carry end-user access credentials.

The Studio access path

GitHub Actions builds and deploys the isolated Studio Worker.

Cloudflare applies the Access policy to the self-hosted Studio origin.

The editor completes the organization’s Access policy before the Studio application loads.

After that edge check, Sanity authenticates the editor and authorizes access to the configured project and dataset.

The self-hosted Studio origin is allowed to communicate with Sanity using credentialed CORS. We keep the exact allowed-origin configuration in internal operations documentation.

The public takeaway is straightforward: protect the authoring interface at the edge, keep deployment credentials separate from editor identity, and let Sanity enforce content-level authorization. Exact Access policy conditions belong in internal runbooks.

Treat Worker bindings as deployment configuration

Keep the Studio Worker configuration narrow: the Wrangler example above serves dist through ASSETS with SPA fallback. Build-time Studio values identify its Sanity project, dataset, and preview target. The public site has a separate Next.js Worker and bindings. Any additional private route or Worker secret in a production installation requires its own access and deployment review.

Put release gates before deployment

The GitHub Actions example validates required values, installs a clean dependency tree, builds Studio assets, checks Cloudflare authentication, and deploys the CI-tested commit. The pull-request CI separately type-checks and builds Studio, then runs a Wrangler dry-run. Keep checks before the deployment; the Studio and public site should have independent release workflows.

The public Next.js app has its own Cloudflare Worker and deployment workflow. The Studio workflow is isolated so a content-model or Studio change can ship without rebuilding the visitor-facing application. That separation also keeps deployment failures legible: an editor app failure is not disguised as a site release failure.

What to verify before calling it production-ready

  • The GitHub environment contains the required Cloudflare and Sanity values, and the API token has only the Worker permissions the workflow needs.

The Studio workspace builds successfully to dist, and the CI gate validates the exact commit deployed. OpenNext belongs to the separate public Next.js app, not this Studio build.

The Studio Worker serves its dist assets, supports client-side routes, and receives the correct Sanity project ID, dataset, and preview URL at build time. Check any optional plugin license independently if your Studio uses one.

The workflow checks out the CI-tested commit, and a manual dispatch remains available for a deliberate recovery deployment.

A post-deploy check confirms the Studio origin loads, editors can authenticate, and Presentation mode reaches the intended public site environment.

The takeaway

Tevpro uses two Cloudflare Worker deployments under one repository: the public Next.js app and an isolated Sanity Studio Worker. GitHub Actions gates the Studio release on CI and deploys the tested commit with Wrangler. The approach gives teams a clear operational boundary between the public site and the authoring application.

Sources

Written by Justin Waldrip

Why work with us

Why Tevpro?

Whether you’re a startup with a bold product idea or an established company seeking a stronger delivery partner, Tevpro delivers results. Our expert consultants specialize in building secure, scalable applications that simplify operations and drive real ROI.

FAQ

Questions about the deployment boundary

The decisions that keep the deployment understandable and recoverable.

Yes. Tevpro builds Sanity Studio as a static application and deploys it to a dedicated Cloudflare Worker. Sanity continues to provide the Content Lake and APIs, while Cloudflare serves the authoring application.

Deployment review

Need a deployment path your team can operate?

Share the application, CMS, hosting constraints, and release process. We will help map a practical delivery path.

We will use this only to follow up on your request.