Payload → Strapi
Model Strapi content types to match your Payload collections, then copy documents with a script. Strapi's import file is for Strapi data, not a Payload database dump.
Should you make this move?
Both platforms have a case. Compare what you gain with what you give up before scheduling the cutover.
Payload
- Open-source collections, blocks, and a Local API keep the content model in code you can host yourself
- Editors get a purpose-built publishing workflow instead of raw files
- There is no one-click importer, so schema, rich text, and media are a scripted project
- 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: There is no one-click importer, so schema, rich text, and media are a scripted project.
What you lose: Open-source collections, blocks, and a Local API keep the content model in code you can host yourself. 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
- Strapi 5 is empty, or you are writing into types you created for this move. strapi import expects Strapi's own archive.
- You can read Payload with the Local API. find is paginated unless you turn pagination off.
- The site already documents Payload → WordPress, and Strapi → Payload is not this guide.
- Admin users and API tokens are not part of a Strapi content export. Create them again.
- Payload stays online until uploads have been copied into Strapi.
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 |
|---|---|---|---|---|
| Collection config | manual | critical | Payload collections are TypeScript. Strapi content-types are their own schema. Nothing reads one and writes the other. | Create the Strapi types first. Keep field names close when you can. Add a payloadId field. |
| Documents | partial | critical | You page through payload.find and create Strapi documents through the document service or REST. Strapi import will not accept the Payload database. | Compare totalDocs with the Strapi count. Upsert on payloadId so you can rerun. |
| Blocks and Lexical | partial | high | Payload blocks and Lexical do not match Strapi's blocks or rich text. You convert them. | Convert one document of each shape. Store the raw field if you may need it. |
| Uploads | partial | critical | Files live on Payload's disk or adapter. Strapi needs them in its upload plugin. | Upload the bytes. Do not leave Strapi fields pointing at Payload's file host. |
| Relationships | manual | high | Ids change. A relationship has to be rewritten after both documents exist. | Import targets first. Second pass sets relations. |
| Localization | manual | high | Payload localized fields are not Strapi i18n locales until you enable the plugin and import each locale. | Enable i18n on the types that need it. Import locale by locale. |
| Access and users | lost | high | Payload access is code. Strapi export skips admin users and API tokens. Editors need new accounts. | Recreate roles in Strapi admin. Send new logins. |
| Globals | manual | medium | A Payload global is one document. Strapi uses a single type for the same idea, and you create it. | Make a single type for header and footer and copy the fields. |
| Hooks | lost | high | Payload hooks do not become Strapi lifecycles. Side effects stop unless you port them. | List hooks that send mail or sync another system. Port the ones you need. |
| Drafts and versions | partial | medium | Taking only published documents drops draft history. Strapi draft-and-publish is a flag you set on the type. | Turn the flag on before import if editors need drafts. Map status on purpose. |
| Frontend client | manual | critical | The app is calling Payload's API. Strapi's REST or GraphQL shape is different even when field names match. | Point a branch at Strapi before you cut readers over. |
Where each thing goes.
| Source | Destination | Method | Notes |
|---|---|---|---|
| Collection | Strapi content-type | manual | Create it before the script runs. |
| Document id | payloadId | transform | The upsert key. |
| Fields | Attributes | transform | Keep names when the types match. |
| Lexical or blocks | Rich text or Strapi blocks | transform | Convert. Do not paste JSON into a string field and call it done. |
| Upload | Media library | transform | Copy bytes, then relate. |
| Relationship | Relation | transform | Second pass. |
| Locale | i18n locale | manual | Enable the plugin first. |
| Global | Single type | manual | One document. |
| Access function | No destination | unsupported | Rebuild roles. |
Make the move recoverable.
Create the source-of-truth backup
Keep a Payload dump and, once Strapi has data, a Strapi export that you can restore.
- Dump the Payload database and copy uploads.
- Record totalDocs per collection.
- After a successful import, run a Strapi export and store it. That archive is for Strapi, not for Payload.
- Note that the Strapi export omits admin users and API tokens.
- Keep checksums.
Proof to capture: Payload dump checksum, totalDocs, and the later Strapi export checksum.
Create Strapi types
Give every Payload collection a destination.
- Add a type per collection you are keeping.
- Add payloadId as a unique string.
- Enable i18n and draft-and-publish only where you need them.
- Create the media library before the script uploads.
Proof to capture: Save the input, output, command or tool settings, warnings, and final item counts.
Copy documents
Move rows without pretending strapi import understands Payload.
- Page until you have read totalDocs.
- Create documents that have no relations.
- Upload files and store the new media ids.
- Update relations. Write failures to a file.
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.
Someone feeds Payload JSON to strapi import
Strapi import reads a Strapi export archive.
- Consequence
- The import fails, or you think it loaded content when it did not.
- Mitigation
- Use a script. Reserve strapi import for a Strapi-to-Strapi restore.
Stop if: The only artifact you have is a Payload database dump and no script log.
The first page of 10 is the whole migration
find returns 10 documents unless you page.
- Consequence
- Most of the site never arrives.
- Mitigation
- Log totalDocs against rows written.
Stop if: Those numbers differ.
Files still sit on Payload
Relations can store a URL instead of an uploaded file.
- Consequence
- Turning off Payload breaks images.
- Mitigation
- Upload bytes and search Strapi for the old host.
Stop if: A sampled page still loads media from Payload.
Do the work in this order.
- Day 1 · inventory
Count and dump
3 hours active1 day elapsedNone waiting- Record totalDocs.
- Dump Payload and copy uploads.
- List blocks and relations.
Stop / go checkpointIs the backup real?
Go when: The dump restores on a scratch database, or you have tested a table count.
Stop when: Uploads were not copied.
- Days 2 to 3 · schema
Strapi types
1 day active2 days elapsedNone waiting- Create types and payloadId.
- Enable i18n if you need it.
- Create an admin user.
Depends on: inventory
Stop / go checkpointCan you create one document by hand?
Go when: A sample type accepts a document and a file.
Stop when: The type builder is still changing field names.
- Days 4 to 6 · copy
Copy documents
1 to 2 days active3 days elapsedUploads waiting- Create plain documents.
- Upload media.
- Set relations.
Depends on: schema
Stop / go checkpointDo counts match?
Go when: Strapi counts match totalDocs, with a written skip list.
Stop when: The script only logged one page.
- Day 7 · client
Point a branch at Strapi
4 hours active1 to 3 days elapsedFrontend work waiting- Read a sample through Strapi.
- Check locales and image hosts.
- Have an editor open Strapi admin.
Depends on: copy
Stop / go checkpointIs the branch acceptable?
Go when: Sample pages match and media is local to Strapi.
Stop when: A relation or a locale is empty.
- Day 8 · cutover
Freeze and switch
2 hours active1 day elapsedDelta copy waiting- Stop Payload writes.
- Copy the delta.
- Send the app to Strapi.
- Take a Strapi export.
Depends on: client
Stop / go checkpointStay on Strapi?
Go when: The delta matches and the Strapi export is stored.
Stop when: Payload changed after the freeze.
Cut over with a way back.
Cutover
Read production content from Strapi, with Payload frozen and files off Payload's storage.
- Freeze Payload. Record the time.
- Copy documents changed after that time.
- Re-check image hosts.
- Switch the app.
- Run strapi export and store the archive. It is your Strapi backup, not a Payload file.
Proof to capture: Freeze time, counts, and the Strapi export checksum.
Rollback
Point the app back at Payload if Strapi content is incomplete.
- Point the app at Payload.
- Leave Strapi data in place.
- Copy any Strapi-only edits back if editors used it.
- Re-open Payload.
- Keep the Strapi export anyway.
Proof to capture: The app reads Payload and Strapi-only edits are listed.
- Counts do not match totalDocs.
- A sampled relation is empty.
- Media still loads from Payload.
- A locale is missing.
Prove the migration worked.
Every blocking check must pass. Capture the evidence before cleanup begins.
| Pass | ID | Check | Method | Expected result | Evidence |
|---|---|---|---|---|---|
CNT-01Blocking | totalDocs | Compare each collection with the Strapi count. | They match or the skip list explains the gap. | The table. | |
PAG-01Blocking | All pages were read | Read the script log for pages and totalDocs. | Rows read equals totalDocs. | The log. | |
REL-01Blocking | Sample relations | Open 10 documents that had relationships. | Each relation resolves. | The ids. | |
MED-01Blocking | Old host is gone | Search Strapi for the Payload file host. | Sampled content does not use it. | The search. | |
LOC-01Blocking | Locales | Open one document per locale. | Fields are filled. | The documents. | |
RICH-01Blocking | One of each block | Compare rendered output with Payload admin. | Sections you listed are present. | The comparison. | |
ADM-01Blocking | Editor login | Sign in as a new Strapi user and save. | The save works. The API token used for import is not the editor's password. | The user id. | |
BAK-01 | Strapi export exists | Confirm the export file opens and is stored outside the server. | The archive is present and its checksum is written down. | The checksum. |
Remove the scaffolding safely.
Safe after: The app has read Strapi for a week and a Strapi export is stored.
- Keep the Payload dump until the retention date.
- Revoke import tokens.
- Leave Payload read-only, then shut it down when the date arrives.
- Remember the Strapi export has no admin users.
- Set the next review date.
Verify against the primary material.
Platform behavior changes. Check these sources and the review dates above before executing a production migration.
- Payload: Local APIAccessed 2026-11-06
- Payload: paginationAccessed 2026-11-06
- Strapi: data importAccessed 2026-11-06
- Strapi: data exportAccessed 2026-11-06