# Project layout and 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.

**Example structure**

```text
my-project/
├─ functions/
│  └─ log-event/
│     └─ index.ts
├─ package.json
├─ pnpm-lock.yaml
└─ sanity.blueprint.ts
```

`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.

**Example structure**

```text
my-project/
├─ frontend/
│  ├─ package.json
│  ├─ pnpm-lock.yaml
│  └─ next.config.ts
└─ sanity/
   ├─ package.json
   ├─ pnpm-lock.yaml
   ├─ sanity.blueprint.ts
   ├─ studio/
   │  └─ sanity.config.ts
   └─ functions/
      └─ log-event/
         └─ index.ts
```

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.

**Example structure**

```text
my-project/
├─ apps/
│  ├─ functions/
│  │  ├─ package.json
│  │  └─ screen-cfp/
│  │     └─ index.ts
│  ├─ studio/
│  └─ web/
├─ packages/
│  └─ shared-utilities/
├─ package.json
├─ pnpm-lock.yaml
├─ pnpm-workspace.yaml
└─ sanity.blueprint.ts
```

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:

**sanity.blueprint.ts**

```
import {defineBlueprint, defineDocumentFunction} from '@sanity/blueprints'

export default defineBlueprint({
  resources:[
    defineDocumentFunction({
      name: 'screen-cfp',
      event: {
        on: ['create']
      },
      src: './apps/functions/screen-cfp'
    })
  ]
})
```

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

**pnpm-workspace.yaml**

```yaml
packages:
  - 'apps/*'
  - 'packages/*'

catalog:
  '@sanity/client': '^7.22.0'
  '@sanity/functions': '^1.2.1'
```

Then in any workspace:

**apps/functions/package.json**

```json
{
  "dependencies": {
    "@sanity/client": "catalog:",
    "@sanity/functions": "catalog:"
  }
}
```

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.

> [!WARNING]
> Non-TypeScript projects bundle full dependencies 
> For npm or yarn projects that doesn’t use TypeScript, the CLI externalizes dependencies and ships them as a `node_modules` folder alongside the source. Per-file bundles are smaller, but the asset includes the full installed packages.

You can override the defaults per resource in the manifest:

**sanity.blueprint.ts**

```typescript
defineDocumentFunction({
  name: 'log-event',
  src: './apps/functions/log-event',
  transpile: false,
  autoResolveDeps: false,
  event: {
    on: ['create'],
    filter: '_type == "event"',
    projection: '{_id}',
    resource: {type: 'dataset', id: 'production'},
  },
})
```

- `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.

**Avoid this layout**

```text
my-project/
├─ studio/
│  ├─ functions/
│  │  └─ log-event/
│  │     └─ index.ts
│  └─ sanity.blueprint.ts
├─ package.json
└─ pnpm-lock.yaml
```

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:

1. Move `sanity.blueprint.ts` to the repository root.
2. Update each `src:` path in the manifest from `'./<name>'` to `'./apps/functions/<name>'`, or wherever the Function code lives.
3. Declare runtime dependencies in the workspace that owns the Function code, not in the root `package.json`.
4. Rebind your local Blueprint config to the existing remote stack:

**Rebind to an existing stack**

```bash
pnpm dlx sanity@latest blueprints init \
  --project-id YOUR_PROJECT_ID \
  --stack-id <ST-yourstackid> \
  --blueprint-type ts
```

1. 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.
2. 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.



## Related changelog entries

Entries are listed newest first. Follow a link (or append `.md` to its URL for a plain-markdown response) when you need to know what changed, when, or why — for example, to summarize recent updates, explain behavior that differs from older documentation, or check whether a fix has shipped.

- [New blueprints and search docs, smarter search with learn content, and personalized code snippets](https://www.sanity.io/docs/changelog/0f93c4e7-bf5b-488d-9726-915bffefc5a8) — *v2026-05-29* — May 29, 2026