Why We Moved Away from a Headless CMS



Lessons from using Strapi for a dozen apps in production
We did not start with a grand architecture. We started with a headless CMS, and for a long time it worked well. It let us move fast, avoid backend overhead, and focus on delivering value. We loved our headless CMS. Until we didn't.
The kind of applications we build
We build data-driven applications in corporate environments.
They collect input from many users across the organization, combine it with internal and external data sources, and turn it into a consistent, structured model. The goal is not to merely display information, but to generate insights for decisions at a management level.
These systems are not static. They include workflows, state transitions, permissions, and context-dependent logic that evolves over time.
We have built a dozen applications like this and continue to do so, often in regulated environments. At that scale, problems that do not show up early become unavoidable.
Why we started with a headless CMS
We needed speed at the beginning.
Strapi gave us fast data modeling, instant APIs, and an admin UI that business power users could work with. Instead of building a backend, we treated it as a database with a REST interface.
That let us sit down with stakeholders, define a model together, and ship something usable almost immediately.
For early versions, this was a clear advantage.
The architecture we used
Strapi acted as both data store and API layer. Content types defined the schema, relations modeled the structure, and the generated REST API exposed the data.
Custom frontends consumed these APIs and implemented application behavior. Some business logic lived in the frontend, and some in custom extensions inside Strapi when necessary.
This worked because it was simple. One system handled data and access. The frontend handled interaction. For a while, that separation held.
What worked well
Using Strapi as a database with a REST API is efficient. You define content types, model relations, and get endpoints immediately.
This makes early development fast. You can align with business stakeholders on the data model and deliver a working system within days. There is little setup and almost no friction.
The modeling itself is accessible and visual, which improves collaboration.
The admin UI removes the need to build internal tooling upfront. Power users can directly edit and manage data, even where no dedicated interface exists yet.
For a long time, this setup let us move faster than we otherwise could have.
Where it started to break
The issues did not appear all at once. They surfaced as the applications grew in scope and complexity.
Performance became unpredictable
We had limited control over how data was fetched.
With relations, it became difficult to understand what queries were executed at the database level. Response times varied, and optimization required guesswork.
We were debugging performance without real control over how data was fetched and optimized at the query level.
Custom data logic spread across the system
The default API stopped being sufficient.
We added custom controllers, endpoints, and queries. Each addition made sense locally, but over time we rebuilt significant parts of a backend inside the CMS.
Logic became fragmented and harder to reason about.
The permissions model did not match our use case
The system is built for editors. We are building applications.
We needed permissions for end users from external identity providers, with dynamic rules based on context and data. This does not map cleanly to the built-in model.
We worked around it by adding logic in places where it does not necessarily belong. It worked, but it was not clean.
Identity provider integration added friction
Authentication and authorization for application users are not first-class concepts.
Integrating external identity providers required additional layers and glue code. Separating identity from permissions was not naturally supported.
Each step moved us further away from the original simplicity.
Versioning and upgrades became costly
The migration from Strapi v4 to v5 was not a simple upgrade. It required planning and effort similar to a project.
When upgrades are expensive, they get delayed. That leads to lock-in to outdated versions over time.
Coupling in the monorepo
The built-in admin UI introduces coupling through its React dependency.
This makes it harder to evolve frontend stacks independently in a monorepo and can force compromises across projects.
The turning point
None of these issues alone would have been enough. But together, they pointed to a mismatch.
We are building applications, but our backend was designed for content. A headless CMS solves content delivery. It does not solve application complexity.
What this is not
This is not a claim that Strapi is a bad tool, or that the concept of a headless CMS is flawed.
For content-driven systems, it is a strong choice. For marketing sites, content platforms, or simple internal tools, it can be exactly right, and we promote it.
It solved our problems well. Until our problems changed.
Where this leaves us
We did not need more CMS features. We needed a different way to model applications.
We moved away from a headless CMS towards a more conventional backend implementation where we have full control over data access, business logic, and permissions.
This shift changed how we structure our systems and how we think about application boundaries. We will cover that in detail in a follow-up article, including what we built instead and what we would do differently from day one.