Strapi, or a content model built in a panel
Most modern content systems need a developer to change the data structure. Strapi does it differently: you build the model in an interface by clicking, and the API together with the editorial panel arises from it automatically.
That difference decides the choice more often than feature comparisons suggest. A team where an editor or a project lead should add a field to a contact form themselves needs exactly this. A team with developers may prefer keeping the model in code.
The project is open and can be run yourself with no licence fees, though the licence is split and that is worth knowing. Most of the code carries the MIT licence, while everything sitting in directories marked as the enterprise edition falls under a separate vendor licence. That is why dependency scanning services report an unrecognised licence for this project rather than MIT. The vendor also offers managed hosting, and the difference between those two routes is larger than it first appears.
What version five changed
Version five introduced changes that require real work during migration, so they deserve knowing before you start.
The most important concerns how content is referenced. Previously every entry had a numeric identifier; now the primary reference is a separate document identifier. The reason is sensible: one document can have a draft and a published version plus versions in several languages, and all of them are the same document.
The consequence is painful during migration. The tool automating the transition converts function calls while being unable to guess new identifiers for content that already exists, so those places need reviewing by hand.
The second change concerns the data access layer. The former interface was retired in favour of a new one built around the document concept. Behaviour shifted along the way: fetching many entries always returns an array, and there is no separate pagination method.
The third concerns publication state. The parameter distinguishing a draft from a published version changed its name and behaviour, which the migration tool handles itself.
Practical advice for migrating: run the automation tool, then search the project for the markers it left in places requiring a decision. Those indicate where thinking is needed, and skipping them produces errors that surface only in production.
Plan the migration on a separate branch too, and test it against a copy of production data rather than an empty development database. The new identifiers concern existing content, so problems only surface once there is a lot of it and it comes from a real site.
Self hosting versus cloud
This is the decision with the largest financial consequences and deserves making deliberately.
The self hosted variant costs nothing in licence fees. You pay for a machine, a database, and file storage, meaning things you either have already or that cost little. On a small site that is a dozen or so dollars a month, on Railway or any other provider.
The managed variant charges per project, across tiers differing in limits. It removes upkeep, updates, and scaling from you, which for a team without an infrastructure person is often worth its price.
Three things deserve pricing before you choose, since intuition misleads in both directions.
The first is the upkeep cost of a self hosted deployment. Updates, backups, monitoring, and reacting when something breaks amount to a few hours a month. At a developer's hourly rate that work is sometimes more expensive than a subscription.
The second is overage charges on the managed variant. Request counts, outbound traffic, and file storage are billed separately above the threshold, so an image heavy site can generate a bill noticeably above the base.
The third is project count. Per project billing means an agency running ten sites pays ten times, while with self hosting one machine serves several smaller ones.
Building the content model
Work starts in a panel where you define content types and fields.
npx create-strapi-app@latest my-projectAfter launching you create an administrator account and build the model by clicking: a content type for articles, fields for a title, body, and image, a relation to an author. The API appears immediately, along with documentation and a panel for entering content.
That is an advantage competitors with a code based model lack, and a drawback in the same place. A model changed by clicking drifts between environments, since a change made locally does not reach production automatically.
The answer is recording the structure in project files, which Strapi does automatically. Model changes made in development mode save as files that go into the repository and run at deployment.
{
"kind": "collectionType",
"collectionName": "articles",
"info": { "singularName": "article", "pluralName": "articles" },
"attributes": {
"title": { "type": "string", "required": true, "maxLength": 120 },
"slug": { "type": "uid", "targetField": "title" },
"body": { "type": "richtext" },
"author": { "type": "relation", "relation": "manyToOne", "target": "api::author.author" }
}
}That file is the single source of truth about the model, so reviewing a change to it is reviewing a change to the database structure. That property is why the rule of changing the model locally only is worth holding to.
From that follows a rule worth adopting from the start: change the model locally only, never in production. The production panel serves for entering content rather than for changing its structure, and that deserves enforcing technically too, by disabling model editing outside development mode.
Permissions and API access
This is the area where the most consequential mistake is usually made, since the defaults are well considered and the first attempt at using the API typically ends with circumventing them.
A newly created content type is not publicly accessible by default. A request to the API returns a refusal, which is correct behaviour and misleading, since it looks like a configuration error. The natural reflex is enabling public access for every operation, to check whether it works.
That is precisely the moment the problem appears. Enabling write operations for the public role means anybody who knows the API address can create, change, or delete content. It gets discovered usually when entries nobody added start appearing on the site.
The right arrangement is simple. The public role gets read access alone, and only for content types genuinely meant to be visible. Write operations require a token, generated in the panel with a limited scope and passed only through environment variables.
Checking takes a moment and is worth doing before every deployment, since the panel shows the settings rather than their effect.
curl -s -o /dev/null -w "read: %{http_code}\n" \
https://cms.mycompany.com/api/articles
curl -s -o /dev/null -w "write without token: %{http_code}\n" \
-X POST -H "Content-Type: application/json" \
-d '{"data":{"title":"test"}}' \
https://cms.mycompany.com/api/articlesThe first call should return 200 and the second 403. A 200 on the second means the public role holds write permission, so anyone who knows the address can add content.
Editorial permissions are a separate layer. By default a person added to the panel sees everything, and roles let you narrow that to particular content types and operations. With a team beyond a few people that is worth using, if only so nobody accidentally deletes a content type along with its contents.
Performance and caching
A few things that under heavier traffic decide whether a site works, and that on a prototype do not matter.
By default a list request returns a limited number of entries, and understanding that limit pays off, since an application fetching "all articles" receives the first few dozen and will not notice the rest is missing.
Expanding relations is the second trap. Fetching articles together with authors, categories, and images generates database queries proportional to the result count, so a list of twenty articles can perform a hundred queries.
Expensive: /api/articles?populate=*
Cheaper: /api/articles
?fields[0]=title&fields[1]=slug
&populate[author][fields][0]=name
&populate[cover][fields][0]=url
&pagination[pageSize]=20The asterisk in the first form expands every relation and every field of each one, including the article body a list never displays. The second fetches exactly what appears on screen. Limiting expanded relations to those genuinely needed is the simplest optimisation in the whole system.
The third thing is caching on the application side. A content system rarely needs to answer on every user visit, since content changes a few times a day. Generating pages statically and refreshing them after a save removes almost all traffic from the server, and with Next.js it requires one revalidation call after publishing.
Mind images too. By default they are served by the same process handling the API, so on an image heavy site that process becomes the bottleneck. Moving files to object storage with a content delivery network settles that once and lowers costs along the way.
Strapi against the alternatives
| Option | Strength | Weakness | Pick it when |
|---|---|---|---|
| Strapi | Model built in a panel, permissive licence, self hosting | Upkeep is yours, migrations between versions | Editors changing the model without a developer |
| Sanity | Content as data, its own query language | Model in code only | Content used in several places at once |
| Payload | Panel inside the application, configuration in code | Needs a developer for every change | A Next.js project with a technical team |
| Contentful | Maturity, support for large organisations | High price, a more rigid model | An enterprise with compliance requirements |
The first row wins in one specific situation worth naming plainly: when a non technical person should change the content structure themselves. It is the only system among these four where that genuinely works, and simultaneously the reason it lands in projects where nobody needs it.
If a developer changes the model anyway, the other three rows offer better tools: model version control, clearer queries, or fewer things to maintain.
It is also worth asking whether content is the main problem here at all. When a project needs sign in, file uploads, and realtime notifications alongside it, a general purpose backend comes out simpler than a content system with authentication bolted on. PocketBase packs all of that into a single executable, with two caveats its author states plainly: the project has not reached version one and does not scale horizontally.
Upkeep and updates
This part decides costs on a longer lived project and gets underrated during selection.
Updates between major versions require work, as version five's changes show. The migration tool helps, while some changes need decisions that cannot be automated.
Plugins are a separate matter. The ecosystem is rich, and compatibility with new versions depends on their authors. During migration it is usually plugins rather than application code that determine when a move is possible, so check the state of every one you use before starting.
The database is the third thing to plan. By default a project starts on a file based database, convenient locally and unsuitable for production. Move to a relational one at the start rather than after the first concurrent write problem.
Files uploaded by editors land on the server's disk by default, which under serverless deployment means they vanish on every deploy. A plugin directing them to object storage settles that once and deserves adding before the first image is uploaded, since moving existing files needs a separate script.
Common mistakes
The first is changing the content model in production. The structure is recorded in project files, so changes made directly on a server drift from the repository and are lost at the next deployment.
The second is leaving a file based database in production. It works locally and fails under concurrent writes and on every deploy to serverless infrastructure.
The third is files uploaded to the server's disk. Under serverless deployment they vanish, and that is usually discovered a week later.
The fourth is migrating without checking plugins. They block a move to a new version more often than application code does.
The fifth is skipping the markers left by the migration tool. They indicate places requiring a decision, and ignoring them produces production errors.
The sixth is choosing the managed variant without pricing overage charges. Requests, traffic, and file storage are billed separately and on an image heavy site can exceed the base.
The seventh is enabling write operations for the public role while first bringing the API up. Anyone who knows the address can then change content, and it is usually discovered once entries appear that nobody added.
FAQ
Is Strapi free?
The community edition is released under a permissive licence with no licence fees, so self hosting means paying only for infrastructure. The vendor also offers managed hosting billed per project, with separate charges for exceeding request, traffic, and storage limits.
What did version five change?
Above all how content is referenced: the primary identifier is now a separate document identifier covering its draft, published version, and translations. The former data access layer was retired, and fetching many entries always returns an array.
How hard is migrating from version four?
The automation tool converts function calls and some parameters while being unable to guess new identifiers for existing content. It marks those places for review, and they are where the work lies. Check the compatibility of every plugin you use before starting too.
Will editors really change the model without a developer?
Yes, and that is this system's main advantage. Do introduce a rule that model changes happen only in the development environment, though, since the structure is recorded in project files and must reach production alongside the code.
Self hosting or cloud?
With one project and a team lacking an infrastructure person, managed hosting usually pays off. With several projects or an existing technical setup, self hosting comes out cheaper, since the licence costs nothing and one machine serves several smaller sites.
Documentation sits on the project site, and version five's changes in the migration section.