Engineering decisions

How to choose a headless CMS

EdgeBrain Studios11 min read

Choose on the editorial workflow, then check the content model. Almost never the other way round. Every headless CMS on your shortlist can store structured content and serve it over an API, which is why the feature grids all look alike. What separates them is what happens when somebody in marketing wants to schedule a post, preview it against the real site, spot a mistake that is already live, and put yesterday’s version back without opening a ticket.

The decision is stickier than it looks, because what resists migration is the content model and the editors’ habits, and no comparison table has a column for either. The last section is about that, and it sets the order of everything before it.

Self-hosted or managed, and what each does to your data

Self-hosted means Payload, Strapi or Directus: you run the database, the CMS process, the backups and the upgrades, content sits in your own Postgres or Mongo, and nobody meters your reads. Managed means Sanity, Contentful, Storyblok, Prismic or Contentstack: somebody else runs the store with a CDN in front of it, and you pay by seat and by usage.

If a security review requires the content database to live inside your environment, the decision is made and most of this article is moot. Self-host. That is the one gate here that overrides everything else, and the bill for it is denominated in engineering time, the same arithmetic as our comparison of Vercel, AWS and a plain VPS.

Otherwise, licensing is what people get wrong, because “open source” is no longer one thing. Payload is MIT. The Strapi self-hosted core is MIT, with a separate Enterprise edition above it. Directus ships under the Monospace Sustainable Core License, MSCL-1.0-GPL, which restricts what it calls Competing Use: making the software available to a party competing with the licensor’s own commercial offerings of it. For a team building their own site that never bites. The point is that the licence is now something counsel reads rather than something you assume.

And one item we have to price honestly, since we build on it: Payload joined Figma. The announcement says Payload remains open source and self-hosting does not change. We take that at face value and still treat a change of ownership as new information. The MIT licence is what makes that risk survivable.

For a small marketing site, managed is usually cheaper all in than self-hosted, and people who say otherwise are not counting their own hours.

Who models the content, and what a change costs later

In Payload the schema is code: a new field is a commit, a migration and a deploy. Sanity’s schema is code too, deployed with the Studio, but the store does not enforce it, so a new field is a deploy with no migration attached. Strapi and Directus let a non-engineer add a field from an admin UI. Contentful, Storyblok and Prismic model content types in a web interface by design.

Marketing scores that axis one way and engineering the other, and both miss the question that costs money: what does a schema change cost nine months after launch, with real content in it? Three costs, one of them code.

The migration. On a relational store, Payload on Postgres being the obvious case, a change generates a real migration that gets committed, reviewed and run in CI. That is friction that pays. A document store removes the step and hands you the bill later, when four years of entries have three shapes and nothing ever stopped them. The same trade at the database layer is in Postgres or MongoDB for a new product.

The backfill. Adding a required field to a type holding four thousand published entries means either a script that fills it or four thousand invalid documents, found by an editor one at a time. Ask whether a scriptable write API can run a backfill without a human in the loop. Nobody demos this.

Everything downstream. A rename is one change plus however many call sites it touches.

The failure we see most is the flexible block field. Marketing asks to build pages, so they get a block picker, and eighteen months later there are forty block types including three near-identical heroes, none deletable because nobody knows which pages use which. No CMS prevents that and every CMS enables it. Preventing it is most of what we do on a Payload build, and would be the same work on any of the others.

The editor experience, which is the axis that decides it

Preview

The demo shows preview working beautifully against the vendor’s own sample front end. Yours is not that. Headless preview needs a draft-aware route in your application, a secret to authorise it, and a way for the CMS to point at the right URL for the document being edited. Managed vendors ship more of that plumbing; self-hosted, you build it. It gets left until the end, and an editor who cannot see a change before publishing stops trusting the system inside a fortnight.

Drafts, scheduling and who may publish

Drafts and scheduled publishing are close to table stakes. Approval workflow is not: on several managed vendors the ability to say “this person drafts, that person publishes” sits behind a tier or a role allowance, roles being a counted quota rather than something you configure freely. Price the tier that supports the number of steps your process actually has.

Rollback, and the half nobody mentions

Version history is common. Two questions separate the implementations: can a non-technical editor restore a version themselves, and does restore cover a delete or only an edit? Then the part that is nobody’s fault and still bites. Content rollback does not roll back your schema, so restoring a document written before a field was renamed hands you old content and components expecting the new shape.

When collaboration decides it outright

Sanity’s editing model handles several people in the same document at once. Payload has drafts, version history and document locking, and not Google-Docs-style multiplayer editing on one record. If two editors genuinely work the same page at once as a daily thing, that difference outranks everything else here and Sanity is the answer. If it happens twice a year, locking is fine and you have saved yourself a metered bill.

The cheapest evaluation available: sit your least technical editor in front of each finalist for thirty minutes with one real task, publishing a real page with a real image and a real link, and watch without helping. The number of questions they ask predicts the next two years better than any row on a grid does.

Localisation is a pricing decision before it is a technical one

Three shapes exist: translate at field level inside one document, duplicate the document per locale, or keep a space or dataset per locale. Field level keeps structure in one place. Document level lets locales diverge editorially, which is what regional teams usually want. Space level separates cleanly and multiplies what you maintain.

Then the part discovered after committing: locales are frequently a metered quota. Reading the published pricing pages on 11 September 2026, Prismic allows two locales on Free and three, four, five and eight moving up through Starter, Small, Medium and Platinum. Storyblok includes two languages on Growth and charges $20 per month for each additional one. Sanity does not meter locales at all, but additional datasets are $999 per dataset per month, which makes a dataset-per-locale design expensive. Settle the shape first, then price vendors against it.

Assets and the pricing model, where it bites

Managed pricing is seats plus usage, and usage is what surprises people. The figures below came off the vendors’ own pricing pages on 11 September 2026. Vendor pricing moves, so read them as shape, not quote.

Sanity publishes a free tier, Growth at $15 per seat per month and a custom Enterprise tier, with 250,000 API requests included then $1 per 25,000, one million CDN requests then $1 per 250,000, and 100GB each of assets and bandwidth then $0.50 and $0.30 per additional GB. Storyblok’s Growth plan is $99 per month billed monthly, includes five seats with extras at $15 each, one million API requests with more at $10 per million, and 400GB of traffic with more at $75 per 250GB. Strapi Cloud is $35, $90 and $450 per project per month, with API request overage at $1.50 per 25,000 and asset bandwidth at $30 per 100GB. Contentful publishes a free tier and a self-serve tier and puts everything above on a custom quote, which is a mechanism rather than a criticism: the bill at the size you expect to be is the thing their site will not tell you.

Seats bite because a per-seat model prices success: the CMS becomes useful, and then an agency wants three logins. Know which vendors’ seats cap out and force a tier jump.

API calls bite differently, and this is the part most teams get wrong. Your request count is a function of your cache hit rate, which is a property of your code rather than your traffic. A well-cached Next.js site fetches content when it revalidates, not when a visitor arrives. A route that quietly opts out of caching turns every page view into a billable request, and the first signal is an invoice, not an error. Why your Next.js site is slow is the same failure seen from the latency side.

Assets carry one extra trap: images served from the vendor’s domain with its transform parameters in the query string make every reference in every document vendor-specific. Some platforms let you point at your own object storage instead.

Where our own picks are the wrong answer

Payload is wrong when nobody on your staff can review a pull request against the config file. A code-first schema is an advantage only if somebody inside your company can read the code; otherwise it makes you permanently dependent on whoever built it, which is the opposite of what you were buying. It is wrong again if you are not on Next.js, because the whole argument for Payload 3 is that it runs inside your App Router app. Serve Astro, Rails or a native mobile client and you are back to an HTTP API and a token, where a managed CMS costs less to think about.

Sanity is wrong when content has to sit inside your own environment, and when a metered bill is genuinely unacceptable against a fixed budget rather than merely uncomfortable.

Which leads to the answer we give more often than either. For a site of eight pages that changes twice a year, edited only by people who can open a pull request, the right CMS is Markdown files in the repository and no CMS at all. We have talked clients out of a CMS purchase on that basis, and it is usually the cheapest advice we give them.

The largest concession is the next one. If you are on WordPress, your editors know it, and your real complaint is speed, the CMS is probably not your problem. A badly queried headless site is slower than a well-cached WordPress one. Diagnose first. If the answer is still a move, our WordPress to Next.js migration work exists for it, in that order.

Why this choice sticks

Every platform here has an export API, and a script that pulls entries out of one and pushes them into another is not the hard part. The code is the cheap half of a CMS migration. Four things are not.

The content model.You modelled to one vendor’s primitives: its references, its block field, its idea of a singleton. The next vendor has different ones. What transfers is intent, and a human has to re-derive it.

The rich text.This is the one that turns a week into a month. Portable Text, Contentful’s rich text tree, Storyblok’s richtext JSON and Payload’s Lexical state are four document formats with different node types and different rules for embedded references and annotations. Plain paragraphs convert cleanly. Inline components, embedded assets and annotated spans convert lossily and get finished by hand.

Asset URLs. Every image reference and transform parameter points at a hostname about to stop being yours.

The editors’ habits.Retraining, relearning where preview lives, rebuilding the approval flow, and a quarter of reduced output. Never in anybody’s estimate, reliably the largest line in it.

The same pattern is in migrating off Bubble to custom code and in auth: build, buy, or self-host. It repeats because it is structural: the reversible part of a platform decision is the code, and the code is the part the sales page talks about.

What this costs to do properly

Our published bands: a marketing site is $7,500 to $15,000 over two to four weeks for up to eight pages, which is where most first CMS builds land. A WordPress to Next.js migration is $16,000 to $28,000, because the content model and the redirects are the work rather than the templates. A scoping sprint is $3,500 for one week, credited in full against a build that starts within thirty days, and here it buys a modelled prototype in two platforms rather than a document of opinions. Our minimum for a build is $7,500. Those figures are estimates in US dollars, not quotations; the quote that follows scoping is fixed in writing before kickoff. The vendor figures earlier are quotations from their own published pages and carry no such note, because they are not ours to qualify.

The compressed decision

If content must live in your environment, self-host and read the licence rather than assuming it. If several people edit one document daily, Sanity. If nobody on staff can review a schema pull request, choose a platform whose content types are edited in a UI. If the site is eight pages of prose maintained by developers, use Markdown files and buy nothing. Otherwise shortlist two, model one real page in each, get preview running on your own front end, and price the tier carrying the seats, locales and approval steps you will have in two years.

Weight the editorial half heavily, because that is the half you cannot migrate. For a second opinion on a shortlist you have drawn up, including the case for staying put, send us the shortlist and the content model. The web work this sits inside is on our web development page.

Got a version of this problem?

Send a paragraph describing what you’re trying to build and what’s currently in the way. We reply inside two working days with a scope, a price and a start date. If we’re not the right people for it, we say that instead.