Most teams discover how locked in they are mid-migration, when the export drops relations nobody checked and the frontend turns out to hold hundreds of queries written in a syntax that exists on exactly one platform. A CMS migration checklist that starts with an exit audit answers "how trapped am I?" before anyone argues about where to go. This one walks developers, architects, and ops leads through six audit sections that break into nine scored areas, a scoring model, and the order to attack the results.
In brief
- Documented export-fidelity gaps include a Strapi transfer bug, ignored content-type filtering flags, and the lack of a native bulk export on some managed platforms, so test a real export against representative content first.
- Query layers create a large rewrite surface: proprietary query languages, link-depth parameters, and relation-resolution syntax all need rewriting, and official SDKs mostly serialize that syntax rather than hide it.
- Post-termination access can disappear quickly: paid managed CMS plans may lose Content Delivery API access immediately on suspension.
- Score each area as portable, partially locked, or fully locked, then start migration with the fully locked areas and target a stack where you own the code, the database, and the asset bucket.
Together, these checks turn vague lock-in concerns into a migration scope you can estimate.
Why You Need a CMS Migration Checklist Before Writing a Single Line of Code
Content management systems (CMSs) store content in structurally incompatible ways. Traditional platforms may use a posts table plus metadata, while managed headless CMS platforms often use typed models, linked entries, or queried documents. Moving between them is extraction, transformation, and loading, never a copy. Budget extraction, transformation, validation, and configuration as separate workstreams rather than treating basic content transfer as the whole migration.
A migration plan assumes you know the destination. An exit audit measures how much of your current stack would survive leaving any destination at all. Run the audit first and the plan writes itself, because the fully locked areas dictate scope. Skip it and you'll find the scope creep in staging, when someone notices a feature is missing.
The audit spans three roles. Developers own the query and SDK inventory. Architects own the content model and integration classification. Ops leads own hosting, secrets, and the contract review. Get them in the same room early. The content model findings change what the developers need to count.
Audit Your Content Model and Data Portability
The schema and a real export run are the two inputs that matter — everything else in the content audit follows from what those reveal about field types, relation fidelity, and what the tool actually exports versus what the docs say it does.
Map Every Content-Type and Its Field Schema
Start by pulling the schema out of the platform into files you can diff. In Strapi 5 the schema already lives in version control at ./src/api/[api-name]/content-types/[content-type-name]/schema.json:
File: ./src/api/[api-name]/content-types/[content-type-name]/schema.json
// File: ./src/api/[api-name]/content-types/[content-type-name]/schema.json
{
"kind": "collectionType",
"collectionName": "my_plugin_articles",
"info": {
"singularName": "article",
"pluralName": "articles",
"displayName": "Article"
},
"options": { "draftAndPublish": false },
"attributes": {
"title": { "type": "string", "required": true },
"body": { "type": "richtext" }
}
}Other platforms require vendor-specific commands or provide no CMS-level schema export. In some traditional CMSs, block types are registered per block, and content is stored as HTML with comment delimiters. Treat any schema that exists only in an Admin Panel or across plugin code as configuration you need to reconstruct.
With the schema in hand, flag every field type that has no standard equivalent. Strapi's Dynamic Zone (type: "dynamiczone") requires a __component discriminator on every entry and has no counterpart elsewhere. Component blocks, slice-based fields, and block grammars sit in the same category. Rich text is its own trap: published portable-text specifications and vendor-specific rich-text formats use different document structures and are not interchangeable. Each of these fields needs a transformation, not a mapping.
Test a Real Data Export
Docs describe what the export tool intends to do. Only a run against production-shaped data tells you what it does. From the project root in Strapi 5 that means:
npx strapi export --no-encrypt --no-compressThe default output is an aes-128-ecb encrypted .tar.gz.enc containing JSON Lines files under entities/, links/, schemas/, and configuration/. Admin users and tokens and third-party media on external storage are excluded; only files in the uploads folder move. Then measure the round trip. Strapi has two critical-severity issues, #16813 and #17343, where media and relations inside components disappear during transfer, and #22590 where a 4-to-5 import fails to create draft counterparts for published records. If your content model leans on components with relations, this is the first thing to verify.
The same exercise elsewhere turns up different gaps. One managed CMS CLI version ignored its content-type filtering flag entirely, silently excluded assets returning 401, 403, or 404, and rejected exported asset references when importing into a different project. Another managed platform has no native bulk export. One team scripting a migration of roughly 100 pages generated 12,271 simultaneous Management API requests and hit widespread 429 responses.
Red flags to record include:
- Binary-only or encrypted-only output with no readable option
- No bulk export path at all
- Rate-limited endpoints with hard pagination caps
Any one of these findings should become a separately estimated extraction or validation task.
Evaluate API and Query Layer Dependencies
The query layer is where migration scope is most consistently underestimated — proprietary filter syntax, relation resolution, and SDK-coupled fetch calls each represent discrete rewrite items that need counting before anyone names a timeline.
Catalog Every API Endpoint Your Frontend Consumes
Grep the frontend, middleware, and serverless functions for every REST and GraphQL call, then tag each one that uses vendor-specific filtering, relation resolution, or field selection. On Strapi 5 that looks like:
GET /api/articles?populate[author]=*&filters[slug][$eq]=tokyo&fields[0]=titleThe populate API parameters, including filters, fields, sort, pagination, locale, and status, are Strapi's own. Other headless CMS platforms may use proprietary query strings, reverse-lookup operators, integer link-depth parameters, or flat relation arrays that require platform-specific reassembly logic. Even platforms exposing standard GraphQL often auto-generate every type name from the content model, so query documents do not port unchanged.
Quantify the rewrite surface as a count: how many queries would break if the API shape changed tomorrow? Strapi 5 gave its own users a live test of this. Attributes moved from data.attributes to the top level of data, publicationState became status, and id became documentId. Isolate your fetching layer and the Strapi-Response-Format: v4 header buys you time to migrate consumers at your own pace. Scatter the calls across components and you rewrite every one. Treat each REST call, GraphQL document, or proprietary query string as a separate rewrite item, then group shared response transformations so they are implemented once rather than repeated across components.
Check for Proprietary SDK and Client Lock-In
An SDK only reduces lock-in if it abstracts a standard protocol. Most CMS clients do the opposite. @strapi/client serializes the same populate/filters/fields object graph into query strings, so every find() call carries Strapi-specific shape. Other managed CMS clients commonly expose proprietary query strings or wrap vendor-specific link-depth behavior in chain modifiers rather than providing a portable abstraction.
The pattern that keeps this survivable is Fowler's anti-corruption layer: "Create an isolating layer to provide clients with functionality in terms of their own domain model." The alternative he names, the Conformist pattern, is what most CMS frontends accidentally implement. If your components call the vendor SDK directly, you're a Conformist, and the count from the previous step is your rewrite estimate.
Review Authentication, Roles, and Access Control
Work through three checks before you touch anything else:
- Export role definitions, the permissions matrix, and user records, and note which of the three your platform actually supports.
- Flag every auth flow that depends on the CMS identity layer, including magic links and other passwordless sign-in, SSO, and SCIM provisioning.
- If your RBAC exists only as UI configuration, write it down now. That document is the spec for whatever you rebuild.
These checks separate portable identity standards from configuration that exists only inside the platform.
How far you get on the first check varies. Some managed headless CMS platforms expose roles, permissions, and policies through management or administration APIs. Others hardcode default roles and document no permission-matrix export. In Strapi 5, role-based access control (RBAC) ships three default admin roles and the permission matrix is configured in the Admin Panel, but custom conditions register in code via strapi.admin.services.permission.conditionProvider.register(), which puts at least the logic in git. Custom roles and field-level permissions are Enterprise features. End-user roles in the Users and Permissions plugin have no documented export either.
On the second check, some managed CMS platforms list single sign-on (SSO) configuration explicitly as non-exportable, alongside OAuth tokens and 2FA settings. Where SSO is plan-gated, record the tier, identity-provider configuration, role mappings, and export path as separate findings rather than treating "SSO supported" as a portability result. Strapi's SSO requires the Enterprise plan or the Growth add-on, but provider credentials and callback URLs live in /config/admin under auth.providers, so they move with the repo. System for Cross-domain Identity Management (SCIM 2.0), the IETF standard for portable identity provisioning, is commonly limited to premium or enterprise tiers.
Portable policy formats exist: OPA/Rego, Casbin, and Cedar.
Inventory Plugins, Integrations, and Custom Code
Every plugin and lifecycle hook contains two parts: platform-specific wiring that doesn't survive a move, and business logic that usually does — separating them is what turns a vague "integration rebuild" into an actual effort estimate.
Separate Portable Logic from Platform-Coupled Code
List every plugin, middleware, and lifecycle hook, then split each one into wiring and logic. The wiring is always platform-coupled: hook callbacks, service-container tags, Strapi's strapi.documents.use(), managed app SDKs, and vendor-specific plugin definitions. The logic inside the callback usually isn't. An Algolia saveObject() call, a SendGrid send, or a slug generator has no CMS dependency.
Strapi 5 specifics matter here. Document Service middlewares cover beforeCreate through afterCount, but bulk actions (createMany, updateMany, deleteMany) never trigger them, and publishing a document fires afterCreate twice (draft and published). Strapi's guidance is to prefer Document Service middlewares over DB lifecycle hooks except for the users-permissions plugin and the upload package. Anything still on Entity Service is deprecated and already a migration item.
For effort estimates, inventory the trigger, payload transformation, external API call, retry behavior, and observability for every integration. Estimate the vendor-specific wiring separately from portable services so the migration plan does not charge twice for logic that can move unchanged. Email and analytics follow the same shape: the send or event call is portable, the trigger registration is not. Every plugin managing forms, search, redirects, SEO metadata, pricing tables, or another key task needs a mapped replacement. Scope those replacements before the build starts, not when staging exposes a missing feature.
Assess Media and Asset Pipeline Portability
Answer two questions: who owns the bucket, and who runs the transforms. Managed headless CMS platforms commonly serve media from vendor-owned asset domains and use proprietary transformation paths. None of those buckets are yours, and migration guidance for these platforms warns that moving assets changes asset URLs. Audit rich text and structured fields for absolute asset URLs, then include URL replacement, binary transfer, and metadata validation in the migration test. Compare alt text, focal points, and custom asset fields explicitly rather than assuming that moving the binary preserves them.
Strapi's Media Library stores binaries wherever the upload provider points. With the AWS S3 provider you supply AWS_ACCESS_KEY_ID, AWS_ACCESS_SECRET, AWS_REGION, and AWS_BUCKET, and the provider works with S3-compatible storage such as Cloudflare R2, Scaleway, and MinIO. The bucket outlives the CMS. On Strapi Cloud the default is the local provider, and Strapi provides limited provider support for third-party upload providers there. If assets live in an external digital asset management (DAM) system with no official package for your CMS, that integration is code you own and rebuild yourself.
For transforms, prefer a service you can swap. imgix and Cloudflare Images in bring-your-own-storage mode both read from your bucket, and Next.js loaderFile in next.config.js lets you change transform providers by editing one file.
Measure Infrastructure and Hosting Constraints
Some managed headless CMS platforms are managed-only, with API-only data access. Their published policies may explicitly rule out downloading and self-hosting the platform. Other products make the editing studio self-hostable while keeping the data store managed. A managed-only platform leaves you no fallback of running it yourself if the vendor's terms, pricing, ownership, or product direction changes. Record that dependency as an infrastructure finding instead of assuming API access is equivalent to deployment control.
Self-hostable options give you direct database access across PostgreSQL, MySQL, MariaDB, and SQLite. Strapi 5 supports four documented database systems: PostgreSQL 14–17, MySQL 8.0–8.4, MariaDB 10.3–11.4, and SQLite 3, with no MongoDB or cloud-native database support. Strapi Cloud provisions PostgreSQL by default but lets you point DATABASE_ variables at an external instance. Other self-hosted headless CMS platforms also expose their databases directly, while some managed cloud versions require an Enterprise support request for equivalent access.
Then check who owns the pipeline. For managed-only products, the vendor owns it. On self-hosted Strapi the deployment documentation lists the secrets your CI must supply: APP_KEYS, API_TOKEN_SALT, ADMIN_JWT_SECRET, JWT_SECRET, TRANSFER_TOKEN_SALT, ENCRYPTION_KEY, and database credentials. The docs add the instruction to "Inject secrets from the CI tool, never from a committed .env file." If you can't name where each of those lives today, that's a finding.
Quantify Contractual and Licensing Risk
Read the termination clause before the renewal notice arrives. ENISA exit guidance recommends negotiating exit, pricing, and SLA clauses and avoiding vendor lock-in without a clear exit strategy. One published managed CMS suspension policy keeps the Content Delivery API alive for 30 days only on free plans, while paid plans go dark immediately. For every managed platform, record:
- The post-termination export window
- The export format
- The API-access period
- The deletion obligations
- Who must initiate the export
NIST portability guidance identifies retained data organization and associated metadata as core portability requirements for bulk cloud-data moves.
For EU-scoped contracts, the EU Data Act (Regulation 2023/2854) has applied since September 12, 2025: a 30-calendar-day maximum transitional period for switching, a two-month maximum notice period, and from January 12, 2027 a complete ban on switching charges.
Open-source CMSs carry a different risk: relicensing. License text alone is not the whole governance risk. A contributor license agreement can enable a single maintainer to relicense third-party contributions. Strapi's Community Edition is MIT, conditioned on not accessing the ee/ directory, which is governed by a proprietary ESLA effective March 18, 2026, and holding no Strapi Cloud account. Strapi also adopted a CLA. Review contribution governance, license-change authority, and enterprise feature boundaries alongside the current license text. Previous open-source relicensing cases show that a CLA can make unilateral relicensing possible, so score a CLA as exposure even where no relicense is announced.
Switching cost includes engineering and retraining, the content freeze window, SEO redirect mapping, and legal exit planning. Build the engineering estimate from the scored query, transformation, integration, identity, media, and infrastructure work rather than applying a generic migration-hour benchmark. Test DNS, cache, rollback, and content-delta procedures before launch. The SEO line is where budgets quietly die: inventory every existing URL, map redirects, compare old and new sitemaps, and validate long-tail pages instead of checking only the highest-traffic routes.
Score Your Lock-In Risk and Prioritize the Migration Path
The audit produces a number; the number produces a sequence — fully locked areas open the project because they're what blow timelines when found late, and portable areas migrate last.
A Simple Scoring Framework
Rate each of the nine areas below on a three-point scale and sum the results.
| Score | Rating | Test |
|---|---|---|
| 0 | Portable | Exportable to a standard format, standard protocol, or infrastructure you own; verified by a real run |
| 1 | Partially locked | Exportable with scripting or manual cleanup; some UI-only config or vendor-specific syntax |
| 2 | Fully locked | No export path, proprietary format with no equivalent, vendor-owned storage or identity, or enterprise-gated escape hatch |
Areas: content model, data export, API queries, SDKs, auth and RBAC, plugins and integrations, media pipeline, infrastructure, and contract and license. As a starting heuristic, a total of 0–5 is low urgency, 6–11 means schedule the migration and start the anti-corruption layer now, and 12–18 means most areas are at least partially locked, several have no escape hatch at all, and the audit findings are already your project plan. Lock-in taxonomy research notes that information-and-data lock-in and brand-specific training both rise with time, so a score that's borderline today gets worse by next renewal.
From Audit to Action Plan
Sequence workstreams by score, hardest first. Fully locked areas are the ones that blow timelines when discovered late, so the query layer rewrite and the content model transformation open the project. Partially locked areas run in parallel: role definitions become a written spec, integrations get split into portable logic and new wiring. Portable areas migrate last, often with a config change.
For the target headless CMS, look for the properties that scored zero in your audit: self-hostable on a database you can query, schema in version control, standard REST and GraphQL over HTTP, free bulk export, and a bucket you own. Strapi 5 is an open-source, headless CMS that meets that list. The Content API exposes REST by default, with GraphQL available via @strapi/plugin-graphql. schema.json files live in git. Data Management is free in development and production, and the S3 provider keeps assets in your account. Be honest about what it doesn't fix: populate and filters are still Strapi-specific, Dynamic Zones have direct or near-direct equivalents in some other CMSs, such as Drupal Paragraphs and Payload Blocks, and the transfer bugs above remain documented as critical. Put an anti-corruption layer in front of Strapi too. No platform scores zero everywhere. Choose the one you can leave.
Putting Your CMS Migration Checklist into Practice
Treat the checklist as a recurring audit rather than a pre-migration ritual. Re-run it annually and before any platform renewal. The scores feed both your migration backlog and your negotiating position, and a documented exit strategy is what ENISA control PM-05 asks for in any case.
When the audit says it's time to move, use a migration guide that matches your current architecture and validate it against the findings from this checklist. If you're already on Strapi 4, the strapi transfer documentation covers the Content-Type filtering flags added in 5.51.0, but the 4 to 5 guide does not mention them.





