Tech

Sanity as a Production CMS: A Practical Architecture Guide

November 29, 2025
10 min read
Sanity as a Production CMS: A Practical Architecture Guide

Introduction: Why Sanity is More Than “Just a CMS”

Modern web apps rarely work with a single monolithic CMS anymore. You have marketing pages, documentation, blogs, landing pages, product content, multi-language variants, and multiple frontends (web, mobile, in-app surfaces).

Sanity positions itself not just as a headless CMS, but as a structured content platform:

  • Content is stored as JSON documents in a globally distributed Content Lake
  • Sanity Studio is a fully customizable React application, not a fixed dashboard
  • Data is queried with GROQ, a powerful JSON-first query language
  • Real-time collaboration, presence, and history are built in

In this article, we’ll take a deep dive into how Sanity works under the hood, how to design robust schemas, how to build a preview-friendly architecture for frameworks like Next.js, and what to watch out for in production.

1. Content Lake: Sanity’s Foundation

What is the Content Lake?

At the heart of Sanity is the Content Lake, a multi-tenant, globally cached datastore for JSON documents. Every document you create in Sanity Studio is persisted there:

  • Each document has an _id and _type
  • Updates are patch-based and stored as a history of mutations
  • Reads go through global CDNs for low-latency access
  • Writes can be streamed in real time (subscriptions)

Conceptually, you can think of the Content Lake as a schema-flexible, document-based database that happens to be CMS-friendly.

Benefits for Developers

  1. Structured by design: Instead of “HTML blobs in a WYSIWYG field”, you store structured JSON (arrays, objects, references).
  2. Frontends stay in sync: Any change in the Content Lake propagates automatically to all frontends that query it.
  3. Real-time features: You can subscribe to document changes for live preview and collaborative editing.

2. Schema Design: Modeling Content Like a System, Not a Page

Sanity’s power comes from its schema definitions, written in TypeScript/JavaScript. A well-designed schema is the difference between a flexible content platform and a messy CMS you regret later.

Basic Document Schema

A simple blog post schema might look like this:

// schemas/post.ts
import { defineField, defineType } from "sanity";

export default defineType({
  name: "post",
  title: "Post",
  type: "document",
  fields: [
    defineField({
      name: "title",
      title: "Title",
      type: "string",
      validation: (Rule) => Rule.required(),
    }),
    defineField({
      name: "slug",
      title: "Slug",
      type: "slug",
      options: {
        source: "title",
        maxLength: 96,
      },
    }),
    defineField({
      name: "excerpt",
      title: "Excerpt",
      type: "text",
      rows: 3,
    }),
    defineField({
      name: "publishedAt",
      title: "Published at",
      type: "datetime",
    }),
    defineField({
      name: "body",
      title: "Body",
      type: "array",
      of: [{ type: "block" }],
    }),
  ],
});

Reusable Object Types

Instead of duplicating fields across multiple document types, you can define objects:

// schemas/seo.ts
export default defineType({
  name: "seo",
  title: "SEO",
  type: "object",
  fields: [
    defineField({ name: "title", title: "Meta Title", type: "string" }),
    defineField({ name: "description", title: "Meta Description", type: "text" }),
    defineField({
      name: "ogImage",
      title: "OG Image",
      type: "image",
      options: { hotspot: true },
    }),
  ],
});

Then reference it in any document:

defineField({
  name: "seo",
  title: "SEO",
  type: "seo",
});

References vs Embedded Objects

A key architectural decision is when to use:

  • Embedded objects (inline type: "object" / of: [...])
  • References (type: "reference" to a separate document)

General guidelines:

  • Use objects for strongly coupled content (e.g., FAQ items in a single FAQ block).
  • Use references for reusable content (authors, categories, reusable sections, modular page blocks).

3. GROQ: Querying the Content Lake

Sanity uses GROQ (Graph-Relational Object Queries) to query JSON documents. It’s designed around JSON rather than SQL tables.

Basic GROQ Query

A simple query to get the latest posts:

const query = `*[_type == "post"] | order(publishedAt desc)[0...10]{
  _id,
  title,
  "slug": slug.current,
  publishedAt,
  excerpt
}`;

Execute it using the client:

import { createClient } from "@sanity/client";

const client = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  apiVersion: "2025-01-01",
  useCdn: true,
});

export async function getLatestPosts() {
  return client.fetch(query);
}

Projections and References

GROQ projections allow you to shape the JSON you return. To dereference authors:

const query = `*[_type == "post"]{
  _id,
  title,
  "slug": slug.current,
  "author": author->{
    name,
    image
  }
}`;

The -> operator follows the reference and inlines the referenced document’s fields in the result.

4. Sanity Studio: Your Custom React App

Sanity Studio is not a static dashboard; it’s a React application you can customize.

Basic Studio Configuration

A minimal sanity.config.ts might look like:

import { defineConfig } from "sanity";
import { structureTool } from "sanity/structure";
import { visionTool } from "@sanity/vision";
import schemas from "./schemas";

export default defineConfig({
  name: "default",
  title: "My Content Studio",
  projectId: process.env.SANITY_STUDIO_PROJECT_ID!,
  dataset: process.env.SANITY_STUDIO_DATASET!,
  plugins: [structureTool(), visionTool()],
  schema: {
    types: schemas,
  },
});

Custom Desk Structure

With the Structure Builder API, you can control how editors see content:

// deskStructure.ts
import S from "@sanity/desk-tool/structure-builder";

export const deskStructure = () =>
  S.list()
    .title("Content")
    .items([
      S.listItem()
        .title("Site Settings")
        .child(S.document().schemaType("siteSettings").documentId("siteSettings")),
      S.divider(),
      S.documentTypeListItem("post").title("Blog Posts"),
      S.documentTypeListItem("author").title("Authors"),
    ]);

Then plug it into structureTool in your config.

This lets you group content types, pin important documents like “Site Settings”, and give editors a clear mental model of your content.

5. Live Preview and Frontend Integration (e.g. Next.js)

Sanity integrates well with modern frameworks like Next.js, Remix, or SvelteKit. The most common pattern is live preview for editors.

Preview Drafts vs Published Content

  • Published content is read via useCdn: true for speed.
  • Draft content uses authenticated requests (useCdn: false, token-based) so editors can see unpublished changes.

You can set up a dedicated preview client:

import { createClient } from "@sanity/client";

export const readClient = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  apiVersion: "2025-01-01",
  useCdn: true,
});

export const previewClient = createClient({
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  apiVersion: "2025-01-01",
  token: process.env.SANITY_API_READ_TOKEN,
  useCdn: false,
});

Then, based on a preview flag, pick the correct client in your data layer.

Subscriptions for Live Updates

For a fully live editing experience, you can use subscriptions (e.g., via @sanity/preview-kit) to stream draft updates to the frontend while the editor types.

This is especially powerful when combined with frameworks that support streaming, server components, or Suspense.

6. Workflows, Validation, and Editorial Safety

When you move beyond a personal blog, editorial safety becomes critical. Sanity provides several layers for this.

Field-Level Validation

Validation logic lives alongside your schema:

defineField({
  name: "title",
  type: "string",
  validation: (Rule) => Rule.required().min(5).max(80),
});

You can also write custom validators:

defineField({
  name: "slug",
  type: "slug",
  validation: (Rule) =>
    Rule.custom((value) => {
      if (!value?.current?.startsWith("/blog/")) {
        return "Slug must start with /blog/";
      }
      return true;
    }),
});

Document-Level Rules and Workflows

While Sanity handles versions and history automatically, you can layer additional logic via:

  • Custom input components
  • Custom document actions (e.g., “Publish & Notify”, “Generate Translation”)
  • External workflow tools integrated via webhooks or APIs

This lets you shape your own review, approval, and publishing flows without giving up the CMS flexibility.

7. Performance, Caching, and Cost Considerations

Sanity’s Content Lake is globally cached, but you still need to think about performance and costs.

Use CDN for Public Content

For public-facing pages:

  • Enable useCdn: true on the client
  • Cache responses at your framework level (e.g., Next.js route caching, Edge caching)
  • Avoid overly broad queries that fetch unused data

Narrow Your Queries

Instead of:

*[_type == "post"]

limit fields and documents:

*[_type == "post" && defined(slug.current)][0...20]{
  title,
  "slug": slug.current,
  publishedAt
}

This reduces payload size and speeds up both the Content Lake and your frontend.

Think in “Content Boundaries”

For large sites, split content into clear domains:

  • Marketing content
  • Documentation
  • Blog
  • Product/catalog data

Each domain can have its own schema modules, GROQ queries, and even separate datasets if needed.

8. Security and Environments

Sanity makes a clear distinction between public datasets and private tokens.

Public Read, Private Write

Typical production setup:

  • Dataset is configured as public for read-only access via CDN
  • Writes (mutations) require authenticated tokens
  • Preview and internal tools use read tokens with restricted scopes

You should never expose write tokens to the browser; keep them server-side or in serverless functions.

Multiple Environments

You can model environments in different ways:

  • Separate datasets: production, staging
  • Separate projects for fully isolated environments

For many teams, a single project with multiple datasets is a good balance between isolation and manageability.

9. Migration and Long-Term Maintenance

Like any content system, Sanity instances evolve over time. You’ll reshape schemas, rename types, and change content models.

Schema Changes

Simple changes (adding optional fields, tightening validation) are easy. For more complex migrations:

  • Add new fields/types alongside old ones
  • Run scripted migrations using the Sanity CLI and migration scripts
  • Clean up deprecated fields once content has been migrated

Document Migrations

A basic migration script might:

  • Read documents of a given type
  • Transform their shape
  • Write them back with updated fields

Because everything is JSON, this is usually a matter of writing small, targeted scripts rather than huge SQL migrations.

Conclusion

Sanity’s combination of Content Lake, highly customizable Studio, and powerful GROQ queries makes it a strong foundation for modern content platforms. Instead of squeezing everything into a “page-based CMS”, you design a content system that can serve multiple frontends and use cases.

To summarize a practical path to production:

  1. Model your domain first, not your pages – design schemas that reflect the actual content entities in your business.
  2. Invest in reusable objects and references to avoid duplication and keep content DRY.
  3. Use GROQ projections to return exactly the JSON your frontends need—no more, no less.
  4. Set up live preview early, so content editors build trust in the system.
  5. Harden validation and workflows as your team grows and more editors join.
  6. Monitor performance and costs, and refine queries and caching strategy over time.

Used well, Sanity becomes the single source of truth for your content, not just “the place where marketing edits text”. It gives both developers and editors a powerful, flexible foundation to ship ambitious, content-rich experiences.

#Sanity#Headless CMS#Content Lake#GROQ#Structured Content#CMS Architecture

GET IN TOUCH

Interested in collaboration?

Say Hello
© 2025 Felix Yu
DESIGNED + CODED