Now booking enterprise content platform builds for 2026. Contact us

All articles Migrations

Migrating from Sanity to Payload CMS: a practical migration guide

Sanity and Payload are both code-first CMSs. The migration exports Content Lake data, converts Portable Text to Lexical, resolves references and assets, and replaces GROQ queries with Payload APIs.


Sanity to Payload is a code-first migration on both sides. That helps. The source schema already lives in code, the target schema will live in code, and the content arrives as JSON documents instead of scraped HTML.

The expensive part sits between those facts. Sanity’s Content Lake exports documents and assets, but it does not export a Payload application. Portable Text is not Lexical. GROQ is not Payload’s query API. References and image fields point at Sanity IDs, and those IDs need a second pass before Payload relationships can be complete.

This guide covers the migration in the order the work normally runs: export the dataset, map schemas, move assets, import documents, resolve references, convert Portable Text, rewrite queries, and list what has to be rebuilt.

For the platform decision before the migration, read Sanity vs Payload. This page is about the mechanics after that decision has been made.

The migration shape

StepSanity sourcePayload targetRule
ExportContent Lake datasetFiles on diskFreeze a repeatable export before writing transforms
ModelStudio schema typesPayload collections and fieldsPreserve the intent and rewrite admin controls for Payload
Assetssanity.imageAsset, sanity.fileAssetUpload-enabled media collectionUpload assets first and record the old asset ID
Documents_type documentsCollection documentsCarry the old _id during import
References{ _ref: "..." }relationship or upload fieldsResolve in a second pass
Rich textPortable Text arraysLexical rich text or Payload blocksMap every style, mark, annotation, and custom object
QueriesGROQ filters and projectionsLocal API, REST, or GraphQLMove projection work into select, depth, or application code
CutoverSanity-powered routesPayload-backed routesPreserve indexed URLs and metadata

The useful advantage is the code-first overlap. Sanity and Payload both treat content models as code, and the team doing the migration is still working in TypeScript or JavaScript files under version control.

Export the Content Lake

Two export paths matter:

The CLI path is the usual migration source because it writes a local gzipped tarball and includes asset downloads by default:

sanity datasets export <dataset> <destination>.tar.gz

The same command can export only selected document types:

sanity datasets export <dataset> <destination>.tar.gz --types <type-a>,<type-b>

For a content-only dry run, the CLI can skip assets:

sanity datasets export <dataset> <destination>.tar.gz --no-assets

The HTTP Export API returns an NDJSON stream from /data/export/{dataset}. It exports all non-deleted documents the token can access, including drafts and asset documents. Sanity’s docs recommend the CLI or JavaScript package for most migration work because those paths include features such as asset downloads and draft handling.

Land the export on disk and transform from there. Do not write import code that queries the live Content Lake every time it runs. A local export lets you rerun the transformer, diff counts, and review failures without changing the source dataset or spending the migration on network retries.

Map schema types to collections

Sanity document types become Payload collections. Sanity object types usually become Payload groups, arrays, or blocks, depending on whether the object is embedded as one field, repeated, or used as a layout unit.

Sanity schema typePayload targetNotes
documentcollectionThe document type name maps to the collection slug
stringtext or selectUse select only when the source schema defines a fixed options list
texttextareaDirect
numbernumberDirect
booleancheckboxDirect
date / datetimedatePreserve timezone handling in the transform
slugtextStore current as the route slug
urltextValidate URL shape in Payload
imageuploadStore crop, hotspot, caption, or attribution as fields where the front end uses them
fileuploadSame asset-first pipeline as images
referencerelationshipSingle relationship
array of referencerelationship, hasMany: trueMultiple relationships
array of blockrichTextPortable Text to Lexical transform
array of objectsarray or blocksUse blocks where the array is a page layout
objectgroupDirect when the object is embedded once
geopointpointConfirm coordinate order in the target field

The schema is not copied line for line. Sanity form groups, Studio preview config, custom input components, validation helpers, and desk structure belong to the Studio. Payload has its own admin config, access functions, hooks, and field options. The migration should carry the content model and editorial intent, then express them in Payload’s collection config.

Carry Sanity IDs during import. Add a temporary legacy-ID field to each target collection, index it, and use it for idempotent upserts and relationship resolution. Remove it after cutover only if you no longer need it for redirects, audit, or support. Name that field in the target model before writing the importer, because relationship resolution depends on it.

Move assets before documents

A Sanity image field contains an asset reference to an asset document. Context-specific data such as crop and hotspot stays on the image object where the image is used. File fields follow the same broad shape through file asset documents.

That means assets move first. For each sanity.imageAsset or sanity.fileAsset, download the binary from the export tarball or the asset URL, create a document in Payload’s upload-enabled media collection, and record a map from Sanity asset document ID to Payload media ID.

Two ID maps keep the import orderly: one from Sanity asset document ID to Payload media ID, and one from Sanity document _id to Payload document ID.

Image usage data needs a separate decision. Sanity stores crop and hotspot on the image field where the image is used; the asset document holds asset metadata. If the front end depends on those values, keep them beside the Payload upload relationship, usually in a group:

Use the field names agreed for the new model. The important shape is an upload relationship plus fields for the Sanity usage data the front end still reads.

Do not assume every asset will be present. Sanity’s CLI documentation says assets returning 401, 403, or 404 are excluded from the export. Treat missing assets as import failures to triage. Blank media fields should block review until someone has checked them.

Import documents in two passes

Sanity references store IDs:

{ "_type": "reference", "_ref": "<sanity-document-id>" }

Payload relationships need Payload document IDs. Those IDs do not exist until the target documents have been created, and circular references make a single ordered import brittle. The import therefore runs in two passes.

Pass one creates every document with scalar fields, rich text placeholders where needed, asset relationships already resolved, and document relationships left empty. It records a map from each Sanity _id to the Payload document ID returned by create or update. The importer should upsert against the agreed legacy-ID field so reruns converge on the same documents.

Pass two updates every document with relationships and any rich-text nodes that needed target IDs. Sanity weak references need a policy at this point. A strong reference expects a target; a weak reference may point at a missing or deleted document. Payload can store optional relationships, but your importer has to decide whether a missing weak target becomes null, a logged warning, or a blocked import.

Convert Portable Text to Lexical

Portable Text is where the migration spends its care. Sanity stores rich text as an array. Text blocks have _type: "block", a style, optional listItem and level, child spans, and markDefs. Custom block objects can sit beside text blocks in the root array, and inline objects can sit inside a text block.

Payload’s default rich text field uses Lexical. The target is still JSON, but it is a different tree with enabled features such as paragraphs, headings, lists, links, uploads, and blocks.

The mapping starts here:

Portable Text itemPayload Lexical targetNotes
Root Portable Text arrayLexical root childrenPreserve order
_type: "block", style: "normal"paragraph nodeChildren become text nodes and inline nodes
_type: "block", style: "h1" through "h6"heading nodeThe style value sets the heading tag
_type: "block", style: "blockquote"quote nodeConfirm the enabled Payload feature writes the shape you expect
listItem: "bullet"list and list item nodesGroup adjacent items with the same list kind
listItem: "number"numbered list and list item nodesPreserve level for nested lists
child spantext nodeSpan text carries the text value
span marks decoratortext formattingMap strong, em, underline, strike-through, and code to enabled Payload text features
span mark that points to markDefslink or custom inline nodeExternal links become link nodes; internal links need relationship resolution
inline object inside a blockinline block or custom Lexical nodeDepends on the Payload editor config
root-level image objectupload node or blockUse the asset ID map
root-level custom objectPayload blockThe object’s _type maps to a block type

Sanity’s Portable Text editor lets teams customize styles, lists, decorators, annotations, inline objects, and block-level objects. Inventory the source schema before building the transformer. A body field with paragraphs, headings, lists, and links is a small transformer. A body field with product cards, inline references, tables, code blocks, and bespoke callouts is a block migration.

Internal links need the same two-pass treatment as relationship fields. A Portable Text annotation can hold a reference to another document. In pass one, emit a placeholder that preserves the Sanity _ref. In pass two, replace it with the Payload document ID after the target map exists.

Create one document by hand in the Payload admin before writing the final transformer. Use the exact rich text features the project will enable, save examples of headings, lists, links, uploads, and blocks, then read the stored JSON. That saved document is the target shape. Building against a guessed Lexical tree is how migrations fail late.

Rewrite GROQ queries

Sanity projects often place a lot of shaping logic in GROQ. Filters choose the document set, projections choose the fields, aliases rename values, and -> follows references.

Payload gives you a different set of tools. The Local API, REST API, and GraphQL API are generated from collections. The Local API is usually the best fit inside a Next.js app because it runs in the same process as Payload and can read the database without an HTTP hop.

The rewrite pattern is mechanical once you inventory the queries:

GROQ habitPayload patternMigration note
_type filterCollection choiceThe document type moved into the collection choice
Field filterswhere constraintsTranslate operators one by one
[0]limit: 1 and first resultKeep null handling explicit
Projectionsselect or application mappingPayload can limit fields, but computed projections move to code
reference->depth or a follow-up queryUse depth carefully on large pages
Array filtersApplication mapping or a targeted queryDo not pull an entire tree only to drop most of it
GROQ aliasesApplication object mappingRename in the front-end data adapter
Cross-dataset referencesExplicit integration workPayload relationships point at configured collections

Two checks make the query rewrite reviewable: list every GROQ string with the route it powers, then record the target Payload query beside it. The migration is finished only when the front end no longer depends on Sanity’s client, query strings, serializers, or image URL builder.

Drafts, versions, and publish state

Sanity exports drafts when the token has access to them. Draft document IDs use Sanity’s draft conventions, while published documents have their own IDs. Payload supports versions and drafts when enabled on a collection, and draft writes use Payload’s _status and versions tables.

Map publish state deliberately. A published Sanity document can become a Payload document with _status: "published" when drafts are enabled. A draft-only Sanity document can become a draft in Payload if the new workflow needs it. Do not merge draft and published records by string manipulation alone. Normalize the source IDs first, decide which version wins for launch, and keep enough metadata to explain that decision during QA.

URLs and front-end rendering

Sanity does not own your final URLs unless your application made it the routing source. The old site might use slug.current, route-builder logic, parent relationships, locale prefixes, or hard-coded route maps. Export the live URL set before cutover and compare it with the new Payload-backed routes.

Preserve paths where the route still makes sense. Where paths change, ship redirects at the framework or CDN layer and verify them against the old URL list. SEO fields also need a target model in Payload: title, description, canonical URL, Open Graph image, robots rules, and any structured data that the old front end rendered from Sanity content.

Rendering code has to move too. Portable Text serializers, Sanity image helpers, GROQ fragments, preview logic, and Studio-specific live editing hooks are application code. Replace them with Payload rich-text rendering, upload rendering, and the preview workflow chosen for the new app.

What does not export

The dataset export gives you content. The migration package is the work around it.

It does not export deleted documents, because the Export API covers non-deleted documents. It does not export your Studio schema as Payload collections. It does not export GROQ queries as Local API calls. It does not export custom input components, desk structure, front-end serializers, Sanity Functions, integrations, deployment config, or editorial process.

Those pieces are rebuilt or retired. This is the main scoping point: a small Sanity project with plain documents and Portable Text moves cleanly, while a Studio with custom inputs, custom previews, heavy GROQ projections, and rich Portable Text objects needs a fuller rebuild around the data move.

A pragmatic checklist

  • Export the dataset to disk with the Sanity CLI or Export API.
  • Confirm whether drafts and assets are included in the chosen export.
  • Inventory every document type, object type, reference, image field, file field, and Portable Text field.
  • Map document types to Payload collections and embedded objects to groups, arrays, blocks, or rich text.
  • Add temporary legacy ID fields for Sanity document IDs and asset IDs.
  • Import assets first and record the asset ID map.
  • Import documents with scalar fields and old IDs.
  • Resolve document references, weak references, and internal rich-text links in a second pass.
  • Convert Portable Text to the exact Lexical shape produced by the project’s Payload editor config.
  • Rewrite GROQ queries into Local API, REST, or GraphQL calls.
  • Replace Portable Text serializers and Sanity image helpers in the front end.
  • Preserve or redirect old URLs and validate metadata before cutover.

How WAYF can help

WAYF is an official Payload partner and Top Contributor. We build Payload and Next.js platforms, and we run CMS migrations where the content model, rich text, URLs, and editorial workflow all have to land together.

If you are weighing a Sanity to Payload migration, start with the Sanity vs Payload comparison. If the operating model points to Payload, book a call and we will scope the export, rich-text transform, query rewrite, and cutover path.

FAQ

  1. Can you migrate from Sanity to Payload?

    Yes. Sanity provides dataset export through the CLI and Export API, and Payload can import documents through the Local API, REST API, or GraphQL. The main work is transforming the model and content: Sanity document types become Payload collections, Portable Text becomes Lexical rich text or blocks, asset references become upload relationships, document references are resolved in a second pass, and GROQ queries are rewritten for Payload's APIs.

  2. Does Sanity export assets?

    The Sanity CLI dataset export creates a gzipped tarball and includes assets by default. The CLI also has a no-assets option, and assets that return 401, 403, or 404 are excluded from the export. The Export API returns non-deleted documents, including drafts and asset documents, as NDJSON; Sanity recommends the CLI or JavaScript package for export work that needs built-in asset downloads.

  3. Does Portable Text map directly to Payload Lexical?

    No. Portable Text and Lexical are both JSON rich text formats, but their trees differ. Sanity stores rich text as an array of blocks, spans, marks, mark definitions, and custom objects. Payload stores Lexical editor state with enabled features such as headings, lists, links, uploads, and blocks. A migration needs a transformer for every Portable Text style, decorator, annotation, inline object, and custom block type in the source schema.

  4. What does not export from Sanity?

    A dataset export gives you content documents and asset documents. It does not turn Studio schema code into Payload collection configs, move GROQ queries, rebuild front-end serializers, carry custom Studio inputs, or reproduce application code and integrations. Deleted documents are outside the Export API result because the endpoint exports non-deleted documents.

Sources


Author

Paul Utr

Co-founder, Chief Growth Officer

Paul has been launching online platforms since his teens, picking up UX and product design by building them. He led the Mailgun redesign at Netguru and was Principal Designer at Ramp Network through its seed-to-Series-B run. At WAYF he leads design and organisational alignment, and watches how language carries through every product we ship.


Rather have it done for you? WAYF runs Sanity to Payload migrations end to end.

We're booking content platform
engagements for 2026.

Twenty-five minutes to walk through the work and decide if we're the right team for it. Scoping and a fixed price come after.