Directus, Payload and Sanity compared on the four things that bite in month seven: how a field rename reaches production, how far permissions bend before you write middleware, how editors see a draft, and what the bill looks like at eight editors.
The ticket is always the same. Someone renames a field in staging, it works, the production deploy goes green — and the column is empty, because Strapi disables the content-type builder when NODE_ENV=production, the schema now lives in Git, and nobody wrote the SQL to carry 1,200 rows across. That's month seven, not day one. The other version of the ticket arrives on the v4 to v5 upgrade, when draft/publish is rebuilt around documents and documentId, the entity service you called in forty places is deprecated, half the community plugins you depend on have no v5 branch, and the custom admin panel you bolted on assumes the old shape of everything. Strapi is decent software. Almost nobody leaves it over what it does on day one.
The usual bake-off — spin up three CMSes, model a blog post, see which admin UI feels nicer — grades the one thing that stops mattering by week three. Four duller questions decide year two: how a field rename travels from a laptop to production without dropping data, how far access control bends before you write middleware, how an editor sees an unpublished draft on the real page instead of a JSON dump, and what the bill looks like at eight editors and 400k API requests a month. Strapi, Directus, Payload and Sanity answer those four completely differently, and none of the answers show up in a demo.
Strapi stores content types as JSON schema files under src/api and applies them on boot. Adding a nullable field is painless. Renaming one is drop-and-recreate unless you hand-write a knex migration in database/migrations, and the failure is silent: the deploy succeeds, the column is empty, and you learn about it from a page that renders a blank hero. Payload treats this as a real problem — the config is TypeScript, and payload migrate:create diffs it against the database and writes an up/down file you can read, edit and run in CI before the container serves traffic. That's the workflow you want the month a marketing site becomes a catalogue with 40k SKUs and a rename turns into a backfill that needs a rollback plan.
Directus goes the other way and it's underrated. It wraps an existing SQL database rather than owning one, so the schema is the database, and directus schema snapshot / schema apply promotes structure between environments as YAML you can review in a pull request. If Postgres already exists and billing, analytics and an internal admin all read from it, Directus is an editing layer over data you own rather than a second source of truth to keep in sync. Sanity sidesteps the question: the content lake is effectively schemaless, so adding a field costs nothing across every document, but a rename is a patch script — sanity migration create batches mutations for you — and there is no psql to open afterwards. Checking whether a field is populated everywhere becomes a GROQ query, not a SELECT count(*) WHERE col IS NULL.
Strapi ships public and authenticated roles, with custom roles and field-level control behind Enterprise. A rule like regional editors publish in their own locale and read the others turns into a policy file plus route middleware — a week of custom code and a test suite nobody writes. Directus expresses the same rule in the UI as a filter object, { locale: { _eq: '$CURRENT_USER.locale' } }, scoped per action and down to individual fields. Where the permission matrix is the product, as it is in most internal tools, that alone decides it.
Payload's answer is access functions that return a boolean or a where-constraint: access: { read: ({ req }) => ({ tenant: { equals: req.user.tenant } }) }, and every read, update and delete inherits it — including the admin panel, because the admin panel is just another consumer of the same API. That property matters more than it sounds. In Strapi the admin is a separate auth surface, so it's easy to lock /api down to nothing and still leave every collection readable through /content-manager to any account with a login and the wrong role.
All four drive a Next.js front end, and all four make draft preview harder than the docs suggest. The shape that works: draftMode behind a short-lived signed token, a publish webhook that calls revalidateTag with a tag per document plus one per collection, and no blanket revalidatePath('/'). Get the tag keys wrong and the loop is editor publishes, sees a stale page, publishes twice more, files a bug against your CDN that is actually a bug in your tagging. Budget a day for it, not an afternoon.
Payload's advantage is structural rather than clever: it runs inside your Next app, so payload.find() queries the database in-process — no HTTP round trip, no second service to deploy, keep warm and pay for. On a content-heavy site that's tens of milliseconds per query and one fewer thing on the on-call rota. Sanity's advantage is editorial: the Presentation tool gives writers click-to-edit overlays on the live page, which is the feature that gets a CMS migration approved by people who will never read your RFC. Strapi sits in between, and its REST layer demands explicit populate on every relation, so a nested page — hero to blocks to media to author — either under-fetches or fans out into a dozen queries per request that look fine locally and saturate the connection pool at a couple of hundred concurrent readers.
Self-hosting Strapi, Directus or Payload is one small container, a managed Postgres and object storage: $60 to $150 a month for production plus staging. Then the hours nobody prices — major-version upgrades, a backup you have actually restored once, and an S3 or R2 adapter wired up on day one so uploads don't disappear with the container filesystem on the next deploy, which is the most common self-host incident we get called about. Sanity inverts the shape: no infrastructure, but per-seat and per-request billing, so the bill tracks editorial headcount and traffic rather than servers. Five editors on a high-traffic marketing site — hosted usually wins once you price the ops time. Fifty internal users touching operational data — hosted is the expensive option by a distance, and the per-seat wall arrives all at once.
Our default: Payload when the front end is Next.js and the model will get complicated with blocks, tenants and locales; Directus when the database already exists or the permission matrix is the hard part; Sanity when editors are the constraint and you don't want a pager; Strapi when the team already runs it and the model really is posts, pages and authors. If you do leave, the migration is a scripted export through Strapi's REST API into the new schema — two to four weeks for a typical marketing site, and rewriting front-end queries is more than half of that, not the content move. Weigh that number against another year of upgrade weekends.
A software studio that ships and maintains its own products — KeepChats, Gwora and MoveProof — and builds the same way for clients. Founded and led by codewithumar.
Talk to the studio →We build software worth keeping — for clients, and for ourselves.
Founded & led by codewithumar