Back to portfolio
Case study · Upgrades

Upgrading Strapi from v4 to v5 in production

Strapi v5 is a breaking major version, with a new data-access API at the core of it. Here is how I upgraded a production CMS from v4 to v5 without losing content or downtime: a stepwise path, 19 lifecycle hooks rewritten, a cloned database to rehearse on, and a cutover that kept production for last.

By Vitor Schiavo Updated October 2026 ~6 min read

Major-version upgrades of a stateful, content-bearing app are where "it is just a version bump" goes to die. Strapi v5 changes the data-access model, so the upgrade is not only new dependencies, it is rewriting how the application reads and writes its own content. The content in the database had to survive, and the site could not go down. This is how I did it.

Step up through the latest v4 first

The first mistake would have been jumping straight from an old v4 to v5. I upgraded the codebase in steps: old v4, then the latest v4, then v5. Landing on the latest v4 first clears the deprecations and surfaces the breaking changes while you are still on a familiar major version, so the final jump to v5 is the only thing left to debug. The same pass moved the runtime to Node 20 and switched the database driver to the maintained one.

The real breaking change: the Document Service

Strapi v5 replaces the old entity-level data API with a new Document Service, and that touches every piece of custom logic. The heaviest part of the migration was rewriting all 19 lifecycle hooks to the new API. Lifecycle hooks are the custom behaviour that runs when content is created, updated or deleted, so getting them wrong is how you silently corrupt data. Each one was migrated and checked individually.

Rehearse on a clone, not on production

Before touching anything live, I cloned the development database into an isolated v5 schema and ran the whole migration against that. The data migration either works on a copy of real content or it does not; finding that out on a throwaway clone is free, finding it out in production is not.

Cut over in phases, production last

The v5 build ran as a new flow alongside the existing v4 one. I cut over environment by environment, starting with the lowest-stakes (sandbox / legacy), validating each before moving on. Production was prepared and then held, cut over only after explicit sign-off. At no point was there a single big-bang switch with no way back.

v4 → v5major upgrade
19lifecycle hooks migrated
0content lost
prod lastphased cutover

The upgrade also made it more resilient

Working this closely with the app surfaced latent fragility: the pods would crash-loop when the CMS upstream had a brief blip, and a burst of traffic to one endpoint could saturate it. I added a shared error handler across every request handler and hardened the database connection pool against transient network blips, plus uptime alerting. The upgrade left the service not just newer, but harder to knock over.

What I would tell someone starting this