Sanity → Strapi
Export a Sanity dataset to NDJSON and asset files, then create Strapi content types and load entries through the API. strapi import only accepts a Strapi archive, and it deletes the destination first.
Should you make this move?
Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.
Sanity
- Content Lake, schema-as-code, and customizable Studio support sophisticated structured content
- Editors get a purpose-built publishing workflow instead of raw files
- GROQ, schema code, and custom editorial tooling demand engineering ownership
- Themes, extensions, and content models add maintenance and portability constraints
Strapi
- An open-source headless CMS with a writable admin, REST, GraphQL, and a readable export archive
- Editors get a purpose-built publishing workflow instead of raw files
- Self-hosting, schema code, and custom importers become your problem when you leave a SaaS CMS
- Themes, extensions, and content models add maintenance and portability constraints
Strapi: An open-source headless CMS with a writable admin, REST, GraphQL, and a readable export archive. This removes a major source-side concern: GROQ, schema code, and custom editorial tooling demand engineering ownership.
What you lose: Content Lake, schema-as-code, and customizable Studio support sophisticated structured content. What you inherit: Self-hosting, schema code, and custom importers become your problem when you leave a SaaS CMS.
Jump to a section
Know the shape of the move.
This timeline assumes
- You can export the Sanity dataset with a logged-in CLI or a read token.
- Strapi is a new project with content types you are willing to design. It is not a drop-in for Sanity Studio.
- You will load content through Strapi's API or a script. You will not point strapi import at the Sanity tarball.
- The site already documents Sanity → Contentful and Sanity → WordPress.
What survives the move.
“Partial” and “manual” are not footnotes. They are work that must be scheduled and verified.
| Item | Outcome | Impact | What happens | Mitigation |
|---|---|---|---|---|
| Dataset archive | partial | critical | npx sanity datasets export writes a gzipped tarball. Inside are data.ndjson, assets.json, images/, and files/. The default mode streams the dataset in one consistent read. Cursor mode is faster and can miss edits made while it runs. | Use the default stream for the final export. Hash the tarball. |
| strapi import | lost | critical | Strapi's import command restores a Strapi export. Schemas must already match. The command deletes existing data, including the database and uploads, before it loads the archive. Admin users are not restored. On shared cloud storage, deleted media records can delete the remote files. | Do not run strapi import on a Sanity file. Build content types, then create entries. |
| Schemas and queries | manual | critical | Strapi's own guide says Sanity schemas and GROQ do not carry over. Relationships have to be mapped again. The guide is a vendor tutorial, updated 14 June 2026, built around a sample project rather than a named customer. | Draw the content types first. Rewrite frontend queries after a pilot entry exists. |
| Assets | partial | critical | By default the Sanity export downloads every image and file asset, including ones nothing references. A 401, 403, or 404 is a warning. The export continues and can still finish. --no-assets drops binaries and strips references, so that file is not a complete copy. --raw keeps references and skips the rewrite. | Export with binaries for the real move. Upload those files through Strapi and relink them. |
| Drafts | partial | high | Sanity's --no-drafts flag exports only published documents. Without it, drafts are in the NDJSON and you must decide what Strapi should publish. | Pick one. Do not import drafts as published pages by accident. |
| Document types | partial | medium | The --types flag limits the export to a comma-separated list. A partial export is easy to mistake for the whole dataset. | For the archive, export every type. Filter later in the script. |
| Editorial history and presence | lost | medium | The export is documents and files. Live presence, comments, and Studio configuration are not Strapi entries. | Finish in-review documents before the final export. |
| Roles | manual | high | Sanity roles do not become Strapi admin roles. strapi import would not restore admin users even from a Strapi archive. | Invite people in Strapi and set permissions before editors log in. |
| API quota | manual | medium | Sanity bills dataset exports against the API quota. Cursor mode uses more requests. | Run the full export when you can watch the quota. Keep the tarball so you do not export twice. |
| Referenced library assets | partial | medium | Linked Media Library assets are included at full size. A 404 often means the library asset was unlinked and later deleted while the dataset still has the asset document. | Read the export warnings. Do not treat a 404 as a failed export, and do not ignore a missing binary you still reference. |
| Webhooks and GROQ listeners | lost | high | Sanity webhooks and GROQ-powered clients keep calling Sanity until you change them. | Point the site at Strapi only after the pilot content type renders. |
Where each thing goes.
| Source | Destination | Method | Notes |
|---|---|---|---|
| Dataset document | Strapi entry | transform | One NDJSON line becomes one create call after the content type exists. |
| Document type | Collection type | manual | Create the type before importing entries. |
| Reference | Strapi relation | transform | Create targets first, then link them. |
| Image asset | Strapi media | transform | Upload the file from the export and store the new id. |
| File asset | Strapi file | transform | Same upload path. Check the 404 warnings. |
| Draft | Draft or skip | manual | Honor --no-drafts or filter draft ids in the script. |
| Slug | UID field | transform | Keep the slug if the public URL should stay. |
| GROQ query | REST or GraphQL query | manual | Rewrite it. The export does not contain a query layer. |
| Project member | Strapi admin user | manual | Invite them. Do not expect the archive to include passwords. |
Make the move recoverable.
Create the source-of-truth backup
Keep a full Sanity tarball that is not your only copy of the content.
- Export with assets, published and drafts, default stream mode.
- Save the tarball outside the Strapi server.
- Read the asset warnings and list any 404 you still need.
- Export schemas from the Studio repo. They are code, not rows in data.ndjson.
- Leave Sanity available until Strapi renders the pilot.
Proof to capture: The tarball opens, data.ndjson has one line per document you expect, and the Studio repo is tagged.
Content types
Give Strapi a place to put Sanity documents.
- List document types from the Studio schema.
- Create a collection for each type you are keeping.
- Add relations only after both types exist.
- Do not run strapi import to do this. That command expects a Strapi archive and deletes current data.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
Entry load
Create entries and upload assets from the export.
- Read data.ndjson.
- Upload images and files first and keep an id map.
- Create entries. Then a second pass for relations.
- Strapi's guide also shows a community CLI for a sample project. Read it before you point it at production.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
The things most likely to hurt.
These are operating limits. Treat every “Stop if” condition as a blocked migration, not a suggestion.
strapi import deletes the project
Someone passes the Sanity tarball to strapi import because the command is named import.
- Consequence
- Strapi wipes the database and uploads, then fails the schema check. Shared cloud buckets can lose files.
- Mitigation
- Use a script. Keep strapi import for Strapi-to-Strapi restores only.
Stop if: The command about to run is strapi import against a non-Strapi file.
Relations point at missing ids
Entries are created before the documents they reference.
- Consequence
- Links are empty and the site looks migrated.
- Mitigation
- Two passes: documents, then relations.
Stop if: A pilot reference is null.
Drafts go live
The NDJSON includes drafts and the script publishes everything.
- Consequence
- Unfinished documents are on the public API.
- Mitigation
- Filter drafts or export with --no-drafts for the public load.
Stop if: A known draft id is published.
Do the work in this order.
- Days 1 to 3 · inventory
Types and queries
4 to 8 hours active2 to 3 days elapsedEditor sign-off waiting- List document types and the GROQ the site uses.
- Decide drafts versus published.
Depends on: Studio repo and a Strapi admin
Stop / go checkpointExport?
Go when: Each public type has a Strapi destination.
Stop when: A page type has no content type plan.
- Days 3 to 5 · backup
Tarball
2 to 6 hours active1 to 2 days elapsedAsset downloads waiting- Run datasets export in stream mode with assets.
- Read 404 warnings.
Depends on: CLI login or token
Stop / go checkpointCreate types?
Go when: data.ndjson and the asset folders exist.
Stop when: The export used --no-assets and you still need files.
- Days 5 to 12 · pilot
One type end to end
10 to 24 hours active4 to 8 days elapsedAPI writes waiting- Create content types.
- Load one type, its images, and its relations.
- Render it on a preview site.
Depends on: Tarball
Stop / go checkpointLoad the rest?
Go when: A reference and an image survive a reload.
Stop when: You were about to run strapi import.
- Days 12 to 15 · cutover
Final export and DNS
4 to 10 hours active1 to 3 days elapsedFinal load waiting- Export again.
- Load the delta.
- Point the site at Strapi.
Depends on: Pilot
Stop / go checkpointStrapi is the CMS?
Go when: The public pages read Strapi and drafts you excluded are absent.
Stop when: A relation is null on a top page.
- Days 15 to 22 · observe
Keep Sanity
2 to 4 hours active7 days elapsedEditors using Strapi waiting- Watch asset 404s and editor permissions.
- Keep the Sanity project until rollback closes.
Depends on: Cutover
Stop / go checkpointLeave Sanity read-only?
Go when: Editors can publish in Strapi and the tarball is stored.
Stop when: A missing asset is still only in Sanity.
Cut over with a way back.
Cutover
The site reads Strapi. Sanity is unchanged and still holds the dataset.
- Freeze Studio edits.
- Export once more in stream mode.
- Load entries, media, then relations.
- Switch the frontend.
- Check a draft did not publish.
Proof to capture: Top pages render from Strapi, relations resolve, and the tarball is archived.
Rollback
Point the site back at Sanity without deleting the dataset.
- Repoint the frontend at Sanity.
- Leave the Strapi project in place for the next attempt.
- Do not run strapi import to undo the load.
- Unfreeze Studio.
- Diff any Strapi edits you want to keep by hand.
Proof to capture: The public site reads Sanity again and the project was not deleted.
- strapi import was run against the wrong file
- Relations are empty
- Drafts are public
- Assets 404
- Editors cannot sign in to Strapi
Prove the migration worked.
Every blocking check must pass. Capture the evidence before cleanup begins.
| Pass | ID | Check | Method | Expected result | Evidence |
|---|---|---|---|---|---|
V-01Blocking | NDJSON lines | Count documents in data.ndjson against the dataset. | The counts match the export mode you chose. | Line count. | |
V-02Blocking | Warning list | Read export warnings and open five files from images/ and files/. | Referenced binaries exist or are listed as accepted losses. | Warning log. | |
V-03Blocking | Collections exist | Open Strapi admin. | Each type you kept has a collection. strapi import was not used. | Admin screenshot. | |
V-04Blocking | Pilot count | Compare a type's document count with Strapi. | They match, minus drafts you filtered. | Count sheet. | |
V-05Blocking | A reference | Open an entry with a reference. | The relation points at the right entry. | Admin relation. | |
V-06Blocking | Unpublished ids | Search Strapi for a known draft id. | It is absent or still a draft. | Search. | |
V-07Blocking | One page | Render a page from Strapi on preview. | The HTML matches the fields you mapped. | Preview URL. | |
V-08Blocking | Editor login | Sign in as a non-admin editor. | They can edit what you allowed and nothing else. | Role test. |
Remove the scaffolding safely.
Safe after: Editors have published in Strapi for a week and the tarball is stored off the server.
- Store the Sanity tarball and the Studio git tag.
- Revoke the export token.
- Remove the migration script's API token.
- Keep Sanity read-only until the rollback date.
- Delete unused Strapi upload tests.
- Rewrite any remaining GROQ client.
Verify against the primary material.
Platform behavior changes. Check these sources and the review dates above before executing a production migration.
- Sanity: export a datasetAccessed 2026-11-03
- Strapi: data importAccessed 2026-11-03
- Strapi: migrate from SanityAccessed 2026-11-03