Migration8 min read

Top 7 Schema Migration Patterns for Live Headless CMS Content

A field rename ships on a Friday. The build passes, the deploy goes green, and then the live site starts rendering blanks where product descriptions used to be, because forty thousand existing documents still carry the old key.

Published July 24, 2026

A field rename ships on a Friday. The build passes, the deploy goes green, and then the live site starts rendering blanks where product descriptions used to be, because forty thousand existing documents still carry the old key. That is the failure mode every migration on a headless CMS is quietly one careless pull request away from, and it is worse when the content is live, versioned, and edited by non-engineers at the same time you are trying to change its shape underneath them.

Sanity treats this class of problem as a first-class concern rather than an afterthought. As the Content Operating System for the AI era, it keeps the schema in your repository, stores content in a schemaless Content Lake that lets old and new shapes coexist, and gives you query-time and codegen tools to reshape data without a maintenance window. The distinction matters: a migration stops being a big-bang event and becomes a reviewable, reversible sequence.

This article ranks seven patterns for migrating live headless content safely, from expand-and-contract field changes to query-time reshaping and event-driven backfill. Each is framed for the engineer who has to run it against production, not the committee that approves it.

1. Expand and contract, the pattern every safe migration is built on

The single most durable migration pattern is expand and contract, sometimes called parallel change. Instead of renaming a field in place, you add the new field, write to both old and new for a transition period, backfill the historical documents, cut reads over to the new field, and only then remove the old one. Each step is independently deployable and independently reversible, which is exactly what you want when the content is serving live traffic.

The reason this pattern is safer on Sanity than on a strictly validated model is that Content Lake does not reject a document for carrying an extra or a legacy field. Old and new shapes coexist in the same dataset while you work, so there is never a moment where existing documents are invalid against the current schema. You add the new field to your defineType schema, ship it, and forty thousand documents keep serving without complaint.

A concrete sequence: you are splitting a single fullName string into firstName and lastName. Deploy the schema with both new fields present alongside the old one. Wire the editor to populate both going forward. Run a backfill script that reads fullName, splits it, and patches the two new fields. Update your GROQ queries and frontend to read the new fields. Confirm nothing references fullName. Remove it from the schema and clean up.

Where it fits poorly: if a change is genuinely trivial and the dataset is small enough to rewrite in seconds, the full six-step dance is overhead. But for anything touching live production content at scale, the discipline pays for itself the first time a backfill goes sideways and you can roll back a single step instead of a whole release.

The rename that never happens

There is no atomic rename on a live content store. What looks like a rename is always add, backfill, cut over, and remove, run as separate deploys. Because Content Lake is schemaless at storage, the intermediate state where both fields exist is legal rather than a validation error, which is what makes the sequence safe to pause or reverse at any step.

2. Query-time reshaping with GROQ projections

The second pattern buys you time. During a migration the underlying documents are in a mixed state: some backfilled, some not. Rather than blocking the frontend until every record is clean, you reshape the data at read time so the client always sees one stable contract. On Sanity this is a GROQ projection, and it is the pattern that most cleanly separates the storage transition from the delivery contract.

The mechanism is a single query that maps whatever exists to the shape the frontend expects. A projection like {..., "title": coalesce(newTitle, title)} returns newTitle when the backfill has reached a document and falls back to the legacy title otherwise, all in one round trip. GROQ lets you ask for exactly the shape you need, including projections, references followed with the arrow operator, and filters, so the reconciliation logic lives in the query rather than being scattered across application code.

That is a meaningful contrast with GraphQL-based headless platforms, where a query returns the declared content-type shape and any old-versus-new field reconciliation has to happen in the client or a resolver you maintain. With GROQ the transitional mapping is one expression you can delete the day the backfill completes.

Concrete example: you are moving from a flat category string to a reference. Your projection resolves category->title when the reference exists and coalesces to the legacy string when it does not. The site renders correctly on day one of the migration, before a single document has been touched, and keeps rendering correctly as the backfill progresses.

Where it fits poorly: query-time coalescing is a transition tactic, not a permanent architecture. Leaving fallback logic in production queries indefinitely hides an unfinished migration and slows every read slightly. Set a deadline to finish the backfill and strip the coalesce.

3. Migration as code, reviewed like any other change

The third pattern is organizational as much as technical: treat the schema as source code and the migration as a pull request. When the content model is a set of portable defineType files in your repository and shipped as part of Sanity Studio, a field change goes through the same review, CI, and rollback machinery as any other code. Nobody mutates production shape by clicking around in an admin UI, and nobody discovers the change after the fact.

This is where Sanity's this-is-a-React-app-you-ship model earns its keep. Because the Studio is code you deploy rather than a fixed hosted editor, the schema definition and the migration script sit side by side in the same repo, versioned together. A reviewer can see, in one diff, that the new field was added, that a backfill script accompanies it, and that the query changes match. That traceability is precisely what a live migration needs when something goes wrong at 2am and the on-call engineer needs to know what changed.

Contrast this with platforms where the model's source of truth is the hosted project. Contentful's Migrations CLI does let you script content-type changes in JavaScript and commit them, which is genuinely good practice, but the runtime model can still drift if someone edits in the web app. Strapi's code-first mode writes schema to disk you can commit, though production changes require rebuilding and redeploying the server. The common risk is UI drift between what the repo says and what production actually runs.

Where it fits poorly: very small teams doing exploratory modeling early in a project may find full migration-as-code ceremony premature. Once real content and real editors exist, though, the alternative is undocumented production changes, which is the thing this pattern exists to prevent.

One diff, whole migration

With schemas as portable defineType files in your repo, the field addition, the backfill script, and the query change land in a single reviewable pull request. The migration is auditable before it runs and revertible after, instead of being a set of clicks in an admin panel that no changelog ever recorded.

4. Type-driven migration with TypeGen

The fourth pattern turns your compiler into a migration safety net. TypeGen reads your schema and your GROQ queries and generates TypeScript types for both. The payoff during a migration is blunt and valuable: when you rename or remove a field, every place in the codebase that still reads the old field stops compiling. The blank-product-description failure from the introduction becomes a red build instead of a production incident.

The workflow is straightforward. You change the schema, regenerate types, and run the type checker. Queries that reference a field that no longer exists, or that expect a string where the shape is now a reference, surface as compile errors with the exact file and line. You fix them before the deploy, not after a customer reports blanks. This closes the loop between the storage change and the delivery code in a way that manual grep-and-pray never reliably does.

Concrete example: you promote a field from optional to required, or change a plain string to Portable Text. TypeGen regenerates, and the frontend components that rendered the old string type now fail to type-check against the block array. You update the rendering, regenerate, and ship with confidence that no reader path was silently missed.

GraphQL-based platforms offer codegen too, and it works, catching a rename depends on regenerating and rebuilding against the updated content type. The Sanity advantage is that TypeGen covers the GROQ query shape, not just the schema, so a projection that reshapes data mid-migration is itself type-checked against reality.

Where it fits poorly: TypeGen protects TypeScript consumers. If a migration also affects untyped external consumers of your API, mobile clients on an older build, third-party integrations, a webhook subscriber, those need their own contract checks. Type safety is necessary but not sufficient at the edges.

5. Staged and event-driven backfill with Content Releases and Functions

The fifth pattern addresses the hardest part of a live migration: actually transforming the data without a maintenance window, and previewing the result before it goes public. Two mechanisms combine here. Content Releases lets you stage a set of changes and preview the reshaped content in the real layout through Visual Editing before publishing, so the migration is validated against the actual frontend rather than a hope. Functions run server-side on document events, letting you normalize, enrich, or backfill records as they are written.

The event-driven angle is what makes this scale gracefully. Rather than a single monolithic script that must touch every document at once, a Function can transform each document the next time it is edited, spreading the migration across normal editorial traffic. You still run a bulk backfill for cold documents, but the long tail of frequently-edited content migrates itself as editors work, and new writes land already in the new shape.

Concrete example: you are introducing a normalized slug on ten thousand articles. A backfill script handles the bulk overnight. A Function on the article document event computes and patches the slug on any article that is touched afterward, so drafts and edits stay consistent while the bulk job runs, and there is never a window where a freshly edited document is missing the new field.

Where it fits poorly: event-driven backfill alone leaves rarely-edited documents untouched indefinitely, so it is a complement to a bulk pass, not a replacement. And previewing through Content Releases assumes your frontend is wired for Visual Editing; if it is not, you get the staging safety but not the in-layout preview. For a live headless migration at scale, though, the combination of a bulk backfill, an event-driven Function for the tail, and a staged preview before publish is as close to zero-drama as the discipline gets.

Preview the migration, then publish it

Content Releases stages the reshaped content so you can see it in the live layout through Visual Editing before it reaches the production dataset, while Functions run per-document transforms on write. Together they turn a migration from a hold-your-breath cutover into a rehearsed, previewable publish.

Schema migration patterns for live headless content, at a glance

FeatureSanityContentfulStrapiHygraph
Migration as code, versioned in GitSchemas live in your repo as portable defineType files in sanity.config.ts, so a field change is a reviewable pull request, not a console click.Contentful Migrations CLI scripts content types in JavaScript, versioned in Git, though the runtime model still lives server-side and drifts if edits happen in the web app.Content-Types Builder writes schema JSON to disk in code-first mode, committable to Git, but production edits require a rebuild and redeploy of the Strapi server.Schema is managed in the web UI or via the Management SDK; migrations are scripted but the source of truth is the hosted project, not your repo.
Zero-downtime rewrites on live dataContent Lake is schemaless at storage, so old and new field shapes coexist. You backfill with a script while the site keeps serving, then flip reads once data is clean.Two-phase migrations (add field, backfill, remove old) are the documented pattern, but strict content-type validation can reject in-flight documents mid-migration.SQL-backed models mean a column rename is a real database migration; large tables can lock or require a maintenance window depending on the connector.Additive schema changes are online; destructive changes to a live model are gated and may need a staged environment to avoid rejecting existing entries.
Query-time reshaping during transitionA GROQ projection maps legacy and new fields in one round trip (coalesce(newTitle, title)), so the frontend reads a stable shape while the backfill runs underneath.GraphQL returns the declared content-type shape; reconciling old and new fields happens in application code, not the query.REST and GraphQL return the model as defined; transitional field mapping is handled in controllers or the client.GraphQL schema mirrors the content model; blending old and new field names is done client-side, not in the query.
Dry-run and preview of the new shapeContent Releases and Visual Editing let you stage the reshaped content and see it in the live layout before publishing the migration to the production dataset.Sandbox environments and Live Preview let you validate a migrated content type before promoting the environment alias.Draft and Publish plus a staging instance let you rehearse, though preview fidelity depends on your own frontend wiring.Environments and preview endpoints let you test a migrated schema branch before promoting it to production.
Type safety after the schema changesTypeGen regenerates TypeScript types from the schema and your GROQ queries, so a renamed field surfaces as a compile error before it ships.GraphQL codegen produces types from the schema; catching a rename depends on regenerating and rebuilding against the new content type.TypeScript types are generated for the schema; coverage of custom controllers and populated relations varies.GraphQL codegen yields typed queries; a renamed field is caught on regeneration against the updated schema.
Automated post-migration cleanupFunctions run server-side on document events to normalize, enrich, or backfill records as they are touched, spreading a migration across real traffic.Scheduled functions or external workers handle backfill; the CMS itself does not run per-document migration logic on write.Lifecycle hooks and cron plugins can normalize records on write, running inside your self-hosted server process.Scheduled or webhook-triggered external jobs perform backfill; per-write transformation is handled outside the platform.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.