The ninety days is a shape, not a shutter

Licences and procurement often complete months before anyone writes a line of code. A contract signs in March and the build starts in June, and there is nothing wrong with that: it's just how enterprise buying works. So I'd rather organise the engagement around your project than around my calendar. If your kick-off is late because you were waiting on a security review, that isn't a problem to be managed, it's Tuesday. The ninety days is the ideal shape, not a shutter that comes down. (My manager is aware of my position on this.)

What the window is actually for is worth stating plainly, because it's not a training course. I want to know what you're building and how you're building it. What I'm really looking for is anti-patterns, early enough to correct course.

That matters more with Strapi than it would with a closed SaaS CMS. Strapi is open source, usually self-hosted, and endlessly customisable, and that's where its superpower comes from. The same property means it can be customised into something performant and maintainable that carries your whole content operation for a decade, or into something that gets slower every quarter and can't be upgraded. Both projects made a series of small, reasonable-looking decisions. The decisions were different.

So this isn't a best-practices list. Best-practices lists are sorted by what's conceptually important. This one is sorted by how often I actually see it, multiplied by what it costs to undo once content exists.

Before the list: what I can and can't tell you

I went back through a year of support tickets and my own mail before writing this, because I didn't want to publish a list of hunches dressed up as findings. Two things came out of that, and the second one is more interesting than the first.

The first is that support data is a bad instrument for this question. Tickets are filed when something breaks, not when something is merely wrong. A content model that will be painful in a year doesn't page anyone. So the failure modes below that are most expensive are also the ones with the least supporting ticket volume, which is exactly backwards from what you'd want, and it's why the frequency language here is "what I see" rather than a percentage.

The second is that where the ticket data is dense, it lines up with the list. Upgrades and data transfer are by a wide margin the two highest-volume technical categories I could measure over the last twelve months, and the v4-to-v5 migration dominates the first of them. That's not a coincidence: those are the two moments where decisions made in month one present their bill.

One more caveat, because it's a real limit. I only see Enterprise projects that engaged with the programme. The ones that go quietly wrong without ever booking a call are invisible to me. If anything, that means this list under-represents the damage.

The content model was designed around the design

The most common thing I see, and the most expensive to unpick. It happens for an entirely understandable reason: the comps exist before the CMS does. Someone opens the Content-Type Builder with a Figma file on the other monitor and builds what's in front of them. You get a Homepage single type with forty fields, an AboutPage, a ContactPage: content types named after routes rather than after things.

It works. That's the trap. It works beautifully for about six weeks, right until the second page needs the same team bios, or marketing wants a landing page that wasn't in the comps, or someone asks for the same product data in a mobile app.

The line I end up giving in nearly every kick-off: think about your content type requirements separately from the design convention. It avoids API bloat, and it's the difference between a content model that survives your next redesign and one that is your next redesign.

What it costs later is that every fix is a migration: not just a schema change but live content moved between shapes, usually across locales, usually against a publishing schedule you can't pause. And the safety nets don't help you here. Content History only records changes made through the Content Manager, so a migration script that mangles four thousand entries leaves you nothing to restore from.

The tell I listen for: your content types map one-to-one onto your sitemap. If they do, we should talk.

Nobody decided who owns the schema

Close second on frequency, and the hardest to fix because it isn't technical.

In Strapi the content model is code. Changes made in the Content-Type Builder write schema.json files into your repository, so schema changes are commits: they branch, they conflict, they deploy. That's the right design. It also means that if four people can open the Content-Type Builder locally, four people are writing to your data model with no review step.

At month four this shows up as two developers having built overlapping content types in separate branches, a field renamed in one environment and not another, and nobody able to say with confidence what production's schema actually is versus what's in main. It never presents as "we have a governance problem." It presents as a failed deploy, and the governance problem is what you find underneath.

The tell: I ask who approves a content-type change and get a pause, then "well, we all just use the Content-Type Builder."

The fix is cheap, boring, and almost nobody does it before it bites. Name one person or one small group who owns the model. Treat schema changes as reviewed pull requests like any other code change. Decide explicitly that content types are built in one place and promoted, not built in three and reconciled.

You bought the Enterprise features and never switched them on

Extremely common, trivially cheap to fix, and it quietly costs you the whole first year.

This is the one I'd least have predicted and it's right there in my own sent mail. Teams buy Enterprise, deploy, build, launch, and SSO, Audit Logs, Review Workflows and Releases are all still sitting there untouched at the six-month mark. Not rejected after evaluation. Never turned on.

It happens because the features were bought by someone who isn't building the thing. Security asked for SSO, procurement asked for the SOC 2 report, and the developer with a deadline is quite reasonably working on the content model instead. Nobody is wrong at any point in that chain, and the outcome is that you pay Enterprise money for a Community experience.

It also compounds with everything below, because those features are the answer to several of the problems in this list. A team that never enabled Review Workflows builds an approval process out of Slack messages and spreadsheets, then has to unpick it later.

The fix is fifteen minutes of someone's attention, which is precisely why it never gets scheduled. Book the feature walkthrough. And book two of them: the developer session and the content-editor session are different conversations, and running one call for both audiences leaves half the room lost and the other half bored.

The API is being made to decide what the front end looks like

Maybe a third of the teams I work with, and the item I'd most want to state carefully, because the naive version of this advice is wrong.

The usual way this gets written up is "never put styling in your content model", and I don't believe that. Dynamic zones are explicitly designed so an editor can compose and rearrange components, and a per-component layout choice is a legitimate thing for a dynamic zone to express. Sometimes a boolean genuinely belongs on a component. Sometimes an enum for background treatment is the pragmatic answer and everybody moves on.

The actual problem is narrower and more useful: the API shouldn't be the thing that decides what the front end renders. Where you can, put that decision in the front end, a logic gate that reads the content it received and derives the treatment from it, rather than a field whose only job is to carry a CSS decision across the wire. If the component has a video, render the video layout. If the list has more than six items, use the grid. That logic lives better in the code that owns the rendering than in a field an editor has to remember to set.

It's a mix, and which way it falls genuinely depends on the situation. But there's a second consideration that I think settles most of the marginal cases, and it isn't technical at all: cognitive load on your editors.

Every flag you add is one more decision a person has to make before they can publish a paragraph. Give them twelve components, each with four presentation toggles, and you've built something where using the CMS requires understanding the schema. The editor's attention should be on the content. If a decision can be made for them by the front end, make it for them.

There is a technical cost to getting this wrong as well, and it's worth knowing. Presentation-shaped components tend to nest, and nested components mean deeper population. Strapi's own REST documentation is blunt about it: in production, populate explicitly and limit depth to two or three levels. A content model that encodes layout tends to blow straight past that without anyone deciding to.

Nobody should need a PhD in content schemas to write a blog post.

Content promotion became a developer's job

Common, and the one where I most often disagree with the plan a team arrives with.

Teams turn up with a content promotion strategy: author in staging, review in staging, push to production. It sounds like good hygiene, it's how code works after all, and then it produces a system where publishing a press release requires a developer.

I'd push back on the premise. In most cases you don't need content promotion, and the paid features exist so that you don't. Draft & Publish gives you unpublished content sitting safely in production. Review Workflows gate who can move it and when. Releases group entries across content types and locales and publish them together, immediately or on a schedule. Content History gives you the way back. Live Preview lets an editor see the rendered result before any of it is public. Between them, the thing people are trying to buy with a staging environment, "nothing goes live until it's approved", is already available inside one environment, and available to the content team without a deployment.

Environments still matter, obviously. They're for code. The confusion is treating content like code and inheriting the whole promotion apparatus along with it.

If you do need to move content between environments, know what the tool actually is. Strapi's Data Transfer feature requires the source and destination to be exact copies of each other except for the data, and a transfer deletes existing data at the destination. It prompts you to confirm, because it's a whole-instance sync. That's a migration and environment-setup tool. It is not an incremental content-promotion mechanism, and the tickets I see where it's being used as one are people discovering that the hard way.

The failure mode, then, isn't really "they forgot to plan their environments". It's an over-engineered deployment strategy that takes publishing away from the people whose job it is.

Customisation went into core instead of middleware

The least frequent thing on this list and by a distance the most expensive. This is the eighteen-month bill.

A team needs behaviour the product doesn't have. Under deadline, with a capable developer available, the fastest route is to reach into the running application and change what it does. It works, it ships, everyone moves on.

The order I'd actually recommend, and this is my view rather than a documented Strapi policy:

  • Middleware first. Global middlewares, route middlewares and Document Service middlewares cover a surprising amount of what people reach into core for. It's the documented extension surface, it survives upgrades, and it's the first thing I ask about when someone describes bespoke behaviour.
  • Then a plugin, if the behaviour is substantial or needs its own admin surface.
  • And if you genuinely need something changed in core, open a PR and talk to us about it. Strapi is open source. If you've found something core does wrong, or doesn't do, you can propose the change, and there's a real chance we agree and merge it. Then it's not your patch any more, it's the product, and it upgrades along with everything else. The contribution route is documented in the repository. I would much rather have that conversation than find out about your fork eighteen months later.

Because here's what happens if you don't. Every upgrade becomes a merge rather than a version bump. When you hit a genuine bug, we can't reproduce it, because what you're running isn't what we ship, so the fix you need stops being a fix anyone can make. And any performance analysis we do together is against a codebase that isn't ours. Something I end up writing more or less verbatim: any bottleneck we identify against a modified core isn't something we can reproduce on our side or fix upstream.

I want to be fair about why this happens, because the uncharitable framing is also the wrong one. Teams patch core because they're capable and under time pressure, not because they're reckless. The less capable team files a ticket and waits. And it's worth saying that the great majority of customisation I see is entirely sensible: this is a small tail of a mostly good story.

What I'd ask for, at minimum, if you've already done it: keep a written inventory of what's patched, and keep a reproducible unpatched case, so that when you need help, help is possible.

Where the performance problems actually come from

Worth a short section, because "Strapi is slow" is a thing people say and it's almost never the whole story.

In the cases I've worked, performance complaints cluster around two moments. The first is scale arriving: a content model that was fine at five locales behaving very differently at seventy-five, or a populate depth that was fine with a hundred entries and isn't with a hundred thousand. The second is migration: problems that were latent for a year surfacing during a v4-to-v5 move, which is also when the ticket finally gets filed.

Both of those are the same story told twice: the decision was made in month one and the bill arrived later. Which is the entire argument of this post.

One practical note, since it comes up every time. Strapi doesn't ship dashboards or metrics for query performance, connection pool state, slow queries or API-level monitoring. That's handled at your application and infrastructure layer, with your own APM tooling. If you're planning for scale, plan for that too: the conversation where we try to diagnose a production slowdown with no telemetry is a conversation neither of us enjoys.

Why the order is what it is

The pattern, looking at the list, is that the cheap-to-fix items are the ones where nothing has happened yet.

A content model with no content in it is a schema edit. A content model with eighteen months of multilingual content in it, a publishing calendar and three downstream consumers is a programme of work with a budget line.

That's the whole argument for spending early time on decisions rather than on building. Not because planning is virtuous, but because these specific decisions have a cost curve that goes nearly vertical, and you can see which ones they are in advance.

Nothing about the change got harder. What changed is everything that now depends on it.

If you're two weeks in, do these six things

  1. Write down your content model without looking at the designs. If you can't, that's the finding.
  2. Name who owns schema changes and how one gets approved. One person is fine. Nobody is not.
  3. Turn on the Enterprise features you're paying for, and book separate walkthroughs for your developers and your editors.
  4. Go through your components and ask, for each presentation flag, whether the front end could work this out for itself, and whether an editor should have to think about it.
  5. Decide that content lives in production, governed by Draft & Publish, Review Workflows and Releases, unless you have a specific reason it can't.
  6. List every customisation you've written and ask which of them could be middleware.

None of it is sophisticated. All of it is cheaper this week than it will ever be again.