How to Run Sanity in a Monorepo Alongside Your Frontend
You add Sanity to your product repo, then watch your CI pipeline grind to a halt because the Studio's build pulls in a second copy of React, a clashing TypeScript version, and a Vite config that fights with your Next.js bundler.
You add Sanity to your product repo, then watch your CI pipeline grind to a halt because the Studio's build pulls in a second copy of React, a clashing TypeScript version, and a Vite config that fights with your Next.js bundler. A push that touches one schema field rebuilds the entire frontend. Local dev means two terminals, two `node_modules` trees, and a constant question: which package owns the content types? The monorepo was supposed to make the content layer feel native to the app. Instead it feels like two projects wearing a trench coat.
This is a solvable structural problem, not a tooling tax you have to pay forever. Sanity is a headless content platform whose Studio is a real React application you ship, which is exactly why it belongs inside the workspace graph rather than bolted on beside it. Treated as a first-class package, the Studio shares your linting, your type checking, and your generated content types with the frontend that consumes them.
This guide walks through laying out the workspace, sharing schema and TypeGen output across package boundaries, isolating build caches so a content change does not rebuild your app, and wiring Visual Editing across the package line. The goal is one repository where the content model and the frontend evolve together.
Why the Studio belongs in the workspace graph, not beside it
The naive monorepo layout drops a `studio/` folder next to `web/` and calls it done. Both directories run `npm install` independently, pin their own framework versions, and treat the boundary as a wall. The result is the failure mode every team hits: duplicated dependencies, divergent TypeScript settings, and CI jobs that cannot tell a schema change from a marketing copy edit, so they rebuild everything on every push.
The fix is to stop thinking of the Studio as an external admin panel and start treating it as one package in a managed workspace. Tooling like pnpm workspaces, npm workspaces, Turborepo, or Nx exists precisely to model a dependency graph between packages and to run tasks only against what actually changed. Sanity Studio is a customizable React application you configure in `sanity.config.ts` and ship yourself, so it slots into that graph the same way your design-system package or your API client does. It has a `package.json`, it has a build step, and it has well-defined inputs and outputs.
The practical layout most teams converge on is three workspace packages. One app package holds the frontend (Next.js, Astro, or Remix). One app package holds the Studio. One shared library package holds the things both need to agree on: the schema definitions and the generated content types. The shared package is the load-bearing decision here. It is what turns two projects sharing a folder into one project with a single source of truth for content shape. Everything else in this guide follows from getting that boundary right, because the boundary is where coupling either helps you or hurts you.
Laying out the workspace: apps, a shared schema package, and clean boundaries
Start with a root `package.json` that declares the workspace, a `packages/*` or `apps/*` glob, and a single pinned version of TypeScript, ESLint, and Prettier hoisted to the root. Hoisting the toolchain is what kills the divergent-config problem at the source: every package extends one `tsconfig.base.json` and one ESLint config, so the Studio and the frontend cannot drift into mutually incompatible settings.
The directory shape that works:
```
apps/web (Next.js / Astro / Remix frontend)
apps/studio (Sanity Studio)
packages/schema (defineType / defineField modules + GROQ queries)
```
The `packages/schema` package exports your `defineType` and `defineField` schema modules and nothing app-specific. `apps/studio` imports those modules into its `sanity.config.ts` to render the editor. `apps/web` imports the same modules so that the build that generates types and the queries that fetch content both reference one canonical schema. When a developer adds a field, they add it in `packages/schema`, and both consumers see it immediately. There is no schema in two places to keep in sync, which is the single most common source of production drift between what editors can enter and what the frontend renders.
Keep the GROQ queries in the shared package too. Co-locating the `defineType` source and the queries that read it means TypeGen can analyze both together and produce result types that exactly match the projections your frontend actually runs. That co-location is the mechanism that makes the next section, type sharing, work without ceremony. The boundary rule is simple: anything that describes content shape lives in `packages/schema`; anything that renders, either the editor or the site, lives in an app.
One source of truth for types: TypeGen across the package boundary
The reason a shared schema package pays off immediately is type safety that crosses the frontend boundary for free. Sanity TypeGen reads your schema and your GROQ queries and emits TypeScript types that describe exactly what each query returns, including projections, references, and nested objects. In a monorepo, you run TypeGen against `packages/schema` and the generated `sanity.types.ts` becomes a shared artifact that `apps/web` imports directly.
The workflow has three steps wired as workspace scripts. First, `sanity schema extract` serializes your `defineType` modules into a schema manifest. Second, `sanity typegen generate` reads that manifest plus your GROQ query files and writes typed results. Third, the frontend imports those types so that a `fetch` against the Content Lake returns a fully typed object rather than `any`. When someone renames a field in the schema package, the frontend stops compiling until the query and the consuming component are updated. The break happens at type-check time in CI, not at runtime in front of a user.
This is the concrete advantage of the Studio being a code artifact rather than a hosted black box. A platform where the schema lives only in a vendor dashboard cannot participate in your type graph; you write your frontend types by hand and hope they match. With the schema as a workspace package, the content model and the code that consumes it share one compiler pass. Wire `sanity typegen generate` into your `dev` watch task and your CI `build` task so the types are never stale: the generated file is downstream of the schema in your task graph, and every consumer rebuilds when, and only when, the schema changes.
Build caching: stop letting a content edit rebuild your frontend
The most visible monorepo pain is CI time, and the root cause is almost always a task graph that does not know which packages a change touched. A content model edit in `packages/schema` should rebuild the Studio and regenerate types. It should not rebuild and redeploy your entire frontend unless a query or component actually consumed the changed field. A README edit in `apps/studio` should rebuild nothing in `apps/web` at all.
This is exactly the problem task runners like Turborepo and Nx solve with content-hash-based caching and an explicit dependency graph. You declare that `apps/web#build` depends on `packages/schema#typegen`, and `apps/studio#build` depends on `packages/schema#build`. The runner hashes the inputs to each task. If the schema package's hash is unchanged, the cached type output is reused and nothing downstream re-runs. On a well-configured pipeline, a Studio-only change skips the frontend build entirely, and a typo fix in a frontend component never touches the Studio.
The payoff compounds with remote caching. Because the Studio is a deterministic build with declared inputs, its output can be cached and shared across CI runs and across developer machines, so a teammate who pulls a branch with an unchanged Studio gets the cached build instead of rebuilding it locally. The discipline that makes this reliable is keeping side-effecting, dynamic behavior out of build inputs and keeping the schema package's exports pure: schema definitions and queries in, manifest and types out. Get the inputs honest and the cache does the rest, turning a five-minute all-rebuild into a sub-minute targeted one.
The cache is only as honest as your boundaries
Local dev: running the Studio and frontend together without two worlds
Two terminals, two dev servers, and a manual ritual to keep them in sync is the developer-experience version of the duplicated-dependency problem. The monorepo's job is to make `dev` a single command that starts both the Studio and the frontend with shared types regenerating on save. With a workspace task runner, `turbo dev` or `nx run-many --target=dev` launches both app packages in parallel, and a TypeGen watch task regenerates `sanity.types.ts` whenever a schema file in `packages/schema` changes.
The genuinely valuable integration is wiring Visual Editing and the Presentation Tool across the package boundary. The Studio's Presentation Tool renders your live frontend inside the editor and overlays clickable regions that jump an editor straight to the field that produced a given piece of content. In a monorepo this is easier than in a split setup, because the frontend dev server and the Studio are already running side by side on localhost and share the same content types, so the overlays resolve against the exact projections the frontend runs. You point the Presentation Tool at `apps/web`'s local URL, add the Visual Editing overlays to the frontend, and edits in the Studio reflect in the embedded preview in real time over the Live Content API.
This closes the loop that the naive layout breaks. Content shape, editor, and rendered output all live in one repository, one install, one dev command, and one set of types. An engineer changing a schema field sees the Studio form update, the frontend types break where they need to, and the live preview reflect the change, all without leaving the workspace. That is the difference between a content layer that feels native to your app and one that feels like a second project you visit.
Deployment, environments, and keeping app and Studio releases independent
A shared repository must not mean shared release cadence. The frontend and the Studio are different artifacts with different deploy targets: the frontend goes to your hosting platform, and the Studio is either deployed to Sanity's hosting via `sanity deploy` or built as a static app and served wherever you like. The monorepo's value is a single version of the content model across both; it is not a mandate that they ship together. Configure two deploy jobs gated on the task graph, so a Studio-only change deploys only the Studio.
Environments are where the shared schema package earns its keep again. Datasets in the Content Lake (production, staging, and per-developer sandboxes) are selected by configuration, not by code, so the same schema package and the same queries point at whichever dataset an environment specifies. A preview deployment of `apps/web` can read from a staging dataset while production reads from production, with identical code and identical types. Content Releases and scheduling then let editors stage and time content changes against a dataset without involving a code deploy at all, which is what decouples the editorial calendar from the engineering release train.
Governance travels with this boundary. Roles & Permissions, Audit logs, and dataset-level access control live in the platform, so putting the Studio in your monorepo does not pull content governance into your application's auth system. The repository unifies how content shape is defined and how the app consumes it; it deliberately leaves access control, data residency, and the content store itself in the managed platform, where SOC 2 Type II and GDPR commitments apply. Sanity functions here as the Content Operating System for a composable stack: one governed content backend, one schema package shared across surfaces, and independent release lanes for the editor and the app.
Monorepo and frontend-sharing fit across headless platforms
| Feature | Sanity | Contentful | Strapi | Payload |
|---|---|---|---|---|
| Editor as a workspace package | Sanity Studio is a React app you configure in `sanity.config.ts` and import schema into; it installs as an app in your workspace graph. | Editing UI is hosted by the vendor; you consume it, not build it, so it is not a package in your monorepo. | Admin panel is a self-hosted React app you can place in a workspace, though it is coupled to the Strapi server runtime. | Admin is a self-hosted Next.js-native app that lives in your repo, designed to colocate with the frontend. |
| Single source of truth for content shape | Schema defined in code via `defineType`/`defineField` in a shared package both Studio and frontend import. | Content model lives primarily in the hosted dashboard; code reflects it but does not own it. | Content types defined in code as schema files on the server, importable but tied to the server package. | Collections defined in TypeScript config in your repo, owned by code like Sanity's schema. |
| Typed query results across the boundary | TypeGen reads schema plus GROQ queries and emits result types the frontend imports, so fetches are fully typed. | GraphQL codegen produces types from the API schema; projections are GraphQL queries rather than GROQ. | Type generation available for the API and entities; query result typing depends on REST/GraphQL tooling you add. | Generated types come directly from your collection config and are first-class in the same TypeScript project. |
| Querying for exact frontend shape | One GROQ query returns the exact shape with projections, `->` joins, and filters in a single round trip. | GraphQL lets you select fields and resolve references, with shape constrained by the generated schema. | REST plus GraphQL; deep relations often need populate parameters or multiple calls to assemble a view. | REST and GraphQL with depth controls; relations resolved via depth or explicit selection. |
| Targeted CI builds | Studio is a deterministic build with declared inputs, so task runners cache it and skip the frontend on schema-only edits. | Hosted editor means no editor build in CI; frontend caching is on you and unrelated to the content layer. | Server plus admin build in your pipeline; cacheable, but the server runtime adds a heavier build to graph. | Admin and frontend can share one Next.js build, which is convenient but couples their build lifecycles. |
| Live preview across the package line | Presentation Tool plus Visual Editing renders the frontend in the editor over the Live Content API with clickable overlays. | Live Preview is available and configured via SDK; click-to-edit overlays involve additional setup. | Preview is configurable via the preview feature; live click-to-edit overlays are not a built-in default. | Live preview supported, with overlay editing depending on your frontend wiring. |
| Content store and governance | Content Lake holds content with Roles & Permissions, Audit logs, data residency, SOC 2 Type II, and GDPR commitments. | Managed platform with roles, audit, and compliance tiers; governance lives in the hosted service. | Self-hosted, so you own the database, hosting, governance, and compliance posture end to end. | Self-hosted on your database and infrastructure; you own access control and compliance. |