Blueprints

Project layout and monorepos

Lay out a Sanity project so Blueprints, Functions, and your application code coexist cleanly in monorepos.

Blueprints organize Sanity infrastructure as code: projects, datasets, CORS origins, robot tokens, roles, and Functions. As your project grows, the location of sanity.blueprint.ts and the shape of your repository start to matter. This guide explains the three filesystem patterns we support, how dependencies behave in each, and which one to pick for a Turborepo or pnpm workspace.

Prerequisites:

  • Familiarity with Blueprints and Functions.
  • The latest sanity CLI, invoked via npx sanity@latest or pnpm dlx sanity@latest.

The rule: lockfile and manifest live together

Your package manager's lockfile and sanity.blueprint.ts (the blueprint manifest) should sit in the same folder. The CLI uses the lockfile in the current working directory to detect which package manager you use. If the lockfile isn't there, the CLI defaults to npm, which fails on pnpm features like catalog: and workspace: dependencies.

In practice:

  • package-lock.json: manifest at repo root, deploy from root.
  • yarn.lock: manifest at repo root, deploy from root.
  • pnpm-lock.yaml: manifest at repo root, deploy from root.

If you can't co-locate them, pass --fn-installer pnpm to blueprints deploy to force the right installer. Co-locating is the cleaner fix.

Three filesystem patterns

Standalone functions project

A small repository whose only purpose is to deploy Functions.

pnpm install populates node_modules at the root. When you run pnpm dlx sanity@latest blueprints deploy, the CLI hydrates each Function's dependencies from that root install and packages them into an asset before uploading.

This setup is fully supported.

Simple monorepo

Two independent directories, each with their own package.json and lockfile. The frontend stands on its own. Everything Sanity-related (Studio, manifest, Functions) lives together under a sanity/ directory. No workspace tooling required.

Deploys run from inside sanity/. The manifest and lockfile are co-located there, so the CLI detects your package manager correctly with no extra flags. The frontend installs and builds on its own track, untouched by Function deploys.

This setup is fully supported.

Multi-application project (recommended for monorepos)

This is the layout Turborepo and pnpm workspaces are built for, and it's where Blueprints belong once they manage more than just Functions.

The manifest sits at the root next to the lockfile. Each application, including the Functions workspace, owns its own package.json. The manifest references each Function via their definers src: './apps/functions/<name>'. For example:

With this layout you can use pnpm's catalog: and workspace: protocols freely:

Then in any workspace:

This setup is fully supported.

Where dependencies live

The CLI looks for dependencies in two places, depending on what it finds.

Function-level. If a Function's directory contains a package.json, the CLI uses only those dependencies. Nothing else.

Project-level. Otherwise, the CLI uses the package.json next to sanity.blueprint.ts. In a pnpm workspace, that root package.json typically declares devDependencies for tooling, and each workspace member, such as apps/functions/package.json, owns the runtime dependencies its code imports.

Functions cannot mix both sources. If a Function has its own package.json, project-level dependencies are invisible to it. To use a project-level package at the function level, declare it in both places.

pnpm strictness changes where deps must live

By default, pnpm enforces strict resolution: a workspace can only resolve dependencies declared in its own package.json, even if those dependencies exist in the root node_modules. Even though the Sanity CLI might be able to find the dependencies up to the root package.json, it’s better to follow the default practice and declare them in the workspace member that owns the Function code, typically apps/functions/package.json.

How the CLI bundles your function

For TypeScript Functions in a pnpm workspace, the CLI bundles inline using Vite. Rollup, which Vite uses for production builds, tree-shakes unused exports. Each Function's bundle contains only the parts of its dependencies its source actually imports.

Non-TypeScript projects bundle full dependencies

You can override the defaults per resource in the manifest:

  • transpile: false is useful when a Function already emits its own build, for example src: './apps/functions/log-event/dist'.
  • autoResolveDeps: false skips dependency hydration entirely.

Asset size and native modules

Function assets are capped at 200 MB. The CLI checks this before upload.

Native Node modules, anything that includes a .node binary such as sharp or better-sqlite3, are rejected at build time. The Functions runtime is Node.js v24.x in a sandboxed environment that doesn't support them. If your Function needs image processing or other native work, use a JS-only alternative, or do the work outside the Function and pass the result(s) in.

Anti-pattern: Manifest nested inside Studio

Don't put sanity.blueprint.ts inside the Studio directory. It breaks the lockfile rule today, and it gets in the way as your Blueprint grows to manage more resources over time.

Today, this breaks the lockfile rule. Editors typically run pnpm dlx sanity@latest blueprints deploy from my-project/studio because that's where the manifest sits. The lockfile is one level up, so the CLI can't detect it and falls back to npm. If any dependency uses catalog: or workspace:, the build fails with EUNSUPPORTEDPROTOCOL.

All three supported patterns above keep the manifest above the Studio. As your Blueprint accumulates more resources (more Functions, CORS origins, robot tokens, roles, datasets), it's easier to extend a manifest that already sits in the right place. Move sanity.blueprint.ts above the Studio directory now to avoid a forced migration later. If you can't restructure right now, pass --fn-installer pnpm on every deploy as a stopgap. The migration steps below cover the move.

Migrate an existing project to root

If your manifest currently lives under apps/studio/ or apps/functions/, here's how to move to the recommended layout without disturbing the deployed stack:

  • Move sanity.blueprint.ts to the repository root.
  • Update each src: path in the manifest from './<name>' to './apps/functions/<name>', or wherever the Function code lives.
  • Declare runtime dependencies in the workspace that owns the Function code, not in the root package.json.
  • Rebind your local Blueprint config to the existing remote stack:
  • Run pnpm dlx sanity@latest blueprints plan. The output should show only ~ update lines, no creates or destroys. Bundles are re-hashed when the manifest moves, but no resources are added or removed.
  • Deploy: pnpm dlx sanity@latest blueprints deploy.

If blueprints plan shows resource creates or destroys, stop and check that the stack ID and project ID match what was previously deployed.

Was this page helpful?