Strapi vs Directus vs Payload: Headless CMS Compared
Databases, APIs, licenses, and self-hosting
Strapi 5.60, Directus 12.6, and Payload 3.92 still separate the editor from the site, but the databases, licenses, and deploy steps in older tutorials no longer match. Checked 01/11/2026.
Strapi 5 is a standalone Node process with its own SQL schema. Directus 12 wraps a SQL database you already have and serves Studio, REST, and GraphQL from that schema. Payload 3.92 installs inside a Next.js app and talks to PostgreSQL, SQLite, or MongoDB through one adapter.

Strapi Community and Payload’s core are MIT. Directus 12 is source-available under the Monospace Sustainable Core License, and an unlicensed instance stays on the Core tier. The public site in front of any of them is a separate deploy, covered in the Web Infrastructure pages.
What a headless CMS separates
A coupled CMS renders the page in the same process that stores the content. WordPress is that shape, and the metadata on those pages is usually a plugin. The WordPress SEO plugins comparison covers that plugin layer.
A headless CMS stops at the content API and an admin UI. The website, app, or other client asks for JSON. REST is on all three. GraphQL is available on all three, with a different install step on Strapi and Payload. Only Directus documents WebSockets for live updates.
Crawlers see the HTML the frontend publishes, whichever editor produced the source. The self-hosted SEO tools comparison is that layer, outside the CMS.
Versions on this check
| Platform | Stable release | Released | Runtime |
|---|---|---|---|
| Strapi | 5.60.0 | 28/10/2026 | Node, own process. Admin on port 1337 |
| Directus | 12.6.0 | Docker tag pushed 27/10/2026 | Node, own process. Studio on port 8055 |
| Payload | 3.92.1 | 27/10/2026 | Next.js. Admin at /admin on port 3000 |
Payload’s npm registry also shows a 4.0.0-beta line beside 3.92.1. The install command below tracks the stable 3.x release. Strapi’s install docs support Node.js 22, 24, and 26, the Active and Maintenance LTS lines, and reject odd-numbered current releases such as 23 or 25.
git-stars.org listed these star counts on 30/10/2026 for Strapi and Directus, and on 31/10/2026 for Payload: Strapi 73,420, Payload 45,606, Directus 38,766. GitHub’s API returned HTTP 403 during this refresh, so those figures are the git-stars.org listing, not a live GitHub reading.
Strapi 5
Strapi 5 is the standalone Node CMS. The Community edition is MIT-licensed and free to self-host, with REST, a GraphQL plugin, role-based access control, draft and publish, the media library, internationalization, and webhooks. The CMS pricing page puts Live Preview, Releases, and Content History on Growth, at $45 per month, and puts review workflows and audit logs on Enterprise. SSO on Growth is an add-on at $150 per month plus $50 per user per month.
New projects default to TypeScript. The content-type definition is still schema.json, written by the admin Content-Type Builder or by hand.
{
"kind": "collectionType",
"collectionName": "articles",
"info": {
"singularName": "article",
"pluralName": "articles",
"displayName": "Article"
},
"options": {
"draftAndPublish": true
},
"attributes": {
"title": { "type": "string", "required": true },
"content": { "type": "richtext" }
}
}
That file lives at src/api/article/content-types/article/schema.json. Strapi 5’s GraphQL schema identifies documents with documentId. Tutorials that still filter on the numeric id from Strapi 4 are aimed at the old API.
Supported databases, from the install docs:
| Database | Recommended | Minimum |
|---|---|---|
| PostgreSQL | 17.0 | 14.0 |
| MySQL | 8.4 | 8.0 |
| MariaDB | 11.4 | 10.3 |
| SQLite | 3 | 3 |
MongoDB is absent. Strapi’s database page says the project will not support a connection to a database the Strapi app did not create. An existing SQL schema is what Directus wraps.
Create a project with the current CLI. --quickstart is deprecated in Strapi 5. --non-interactive needs a directory name and skips the prompts, including the SQLite default.
npx create-strapi@latest my-project --skip-cloud --non-interactive --use-npm
cd my-project
npm run develop
npx create-strapi-app@latest still runs the same installer. The admin is at http://localhost:1337/admin. SQLite content for a self-hosted project is stored under .tmp/ in the project folder.
GraphQL is a separate package. After it is installed, the sandbox is at /graphql. The documented plugin config sets subscriptions to false.
npm install @strapi/plugin-graphql
Strapi does not publish an official Docker image. strapi/strapi is not the Strapi 5 path. The Docker docs say to build an image from the project Dockerfile, and they point at the community generator npx @strapi-community/dockerize@latest for a starting Dockerfile and Compose file. That generator is community-maintained.
Strapi 5.60.0, released 28/10/2026, includes per-content-type webhook entry events and an audit-log entry for SSO setting changes. The same release notes record a heap bug in an MCP code path, where every request built the schemas of all tools. This page does not document how that MCP server is enabled.
Directus 12
Directus reads a SQL database and generates REST and GraphQL from the tables. The project describes this as wrapping the database you already have. A table you create shows up as /items/ plus the collection name. Studio is the Vue admin. Flows are the visual automation builder. Files can sit on local disk, S3, Google Cloud Storage, or Azure. For a bucket that should stay off AWS, the self-hosted S3 comparison lists the current servers.
The npm package lists Postgres, MySQL, MariaDB, MS SQL, SQLite, OracleDB, and CockroachDB. MongoDB is not one of them.
CREATE TABLE articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT,
published_at TIMESTAMP
);
Directus then exposes that table as /items/articles. The statement above is MySQL (AUTO_INCREMENT). On PostgreSQL the id column is integer generated always as identity. Moving the data out later is a database export, because the rows are already ordinary SQL.
Directus 12 changed the license from Business Source License 1.1 to the Monospace Sustainable Core License (MSCL) 1.0, which Directus describes as source-available and converting to GPLv3 after four years. The SDKs stay MIT. Self-hosted instances without a license key run on the Core tier. The version 12 breaking-change notes say that, once any grace period has ended, SSO stops working, custom permission rules are ignored, and connections to a custom or self-hosted LLM stop working. Upgrades from earlier versions get 30 days before that enforcement. New instances are enforced immediately.
The Open Innovation Grant, documented on the pricing page, gives organizations under $5 million annual revenue and fewer than 50 employees full use at no software cost. Self-hosted grant use is free. Grant use on Directus Cloud is listed at $99 per month for hosting.
WebSockets are real and off by default on self-hosted projects. The realtime docs set WEBSOCKETS_ENABLED to false unless you turn it on. Directus Cloud enables realtime without that variable. The endpoint is /websocket. From Directus 12.5, WebSocket connections are checked against CORS_ORIGIN, so a missing origin is rejected. A liveness probe is GET /server/ping, which returns pong and does not need authentication. /server/health checks dependencies, requires a static token, and returns 403 when the caller is anonymous.
Two-factor authentication is policy-based. The version 12 notes say a Require 2FA policy now follows the policies that actually apply to the login, including a policy on a parent role or on the user, not only the primary role.
The Docker image is directus/directus. Pin a tag. directus/directus:12.6.0 was pushed to Docker Hub on 27/10/2026. The Compose file below follows the Postgres example in Directus’s Docker guide. The image tag is pinned to 12.6.0, PUBLIC_URL points at localhost, and WEBSOCKETS_ENABLED is set so the socket is on. Replace every placeholder before you run it. The published image no longer includes npm or npx, so an extension install that used to run inside the container has to happen at build time. The guide’s healthchecks also set start_interval to 5s.
services:
database:
image: postgis/postgis:17-3.5
volumes:
- ./data/database:/var/lib/postgresql/data
environment:
POSTGRES_USER: "directus"
POSTGRES_PASSWORD: "directus"
POSTGRES_DB: "directus"
healthcheck:
test: ["CMD", "pg_isready", "--host=localhost", "--username=directus"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
cache:
image: redis:7
healthcheck:
test: ["CMD-SHELL", "[ $$(redis-cli ping) = 'PONG' ]"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
directus:
image: directus/directus:12.6.0
ports:
- 8055:8055
volumes:
- ./uploads:/directus/uploads
- ./extensions:/directus/extensions
depends_on:
database:
condition: service_healthy
cache:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget --spider -q http://localhost:8055/server/ping || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
environment:
SECRET: "REPLACE_WITH_YOUR_SECRET"
DB_CLIENT: "pg"
DB_HOST: "database"
DB_PORT: "5432"
DB_DATABASE: "directus"
DB_USER: "directus"
DB_PASSWORD: "directus"
CACHE_ENABLED: "true"
CACHE_AUTO_PURGE: "true"
CACHE_STORE: "redis"
REDIS: "redis://cache:6379"
ADMIN_EMAIL: "REPLACE_WITH_YOUR_EMAIL"
ADMIN_PASSWORD: "REPLACE_WITH_YOUR_PASSWORD"
PUBLIC_URL: "http://localhost:8055"
WEBSOCKETS_ENABLED: "true"
WEBSOCKETS_ENABLED is set here so the example matches a realtime setup. Leave it unset, or set it to false, when you do not want the socket. The first request to port 8055 opens Studio’s admin onboarding, unless ADMIN_EMAIL and ADMIN_PASSWORD already created that user.
Payload 3
Payload 3 is a Next.js application. npx create-payload-app scaffolds one, and the admin is a route in the same app, at http://localhost:3000/admin after pnpm dev or npm run dev. Collections, access rules, and hooks live in TypeScript. The Local API queries the database from server code in that process, so a server component does not have to call the REST API.
The installation doc requires Node.js ^22.23.4 || >=24.22.1, pnpm or npm or Yarn 2 or newer, and one of these Next.js ranges: 15.2.9 through the rest of 15.2, 15.3.9 through the rest of 15.3, 15.4.11 through the rest of 15.4, or 16.2.6 and later. The get-started marketing page still says Node.js 20.9.0 or newer. The installation page is the one that lists the ranges the project will run on.
Official database adapters are @payloadcms/db-postgres, @payloadcms/db-sqlite, and @payloadcms/db-mongodb. The docs call MongoDB and Postgres the common pair, and SQLite is a supported adapter for local projects. There is no MySQL adapter in that list. Postgres is a stable adapter. Older write-ups that call Postgres a beta, and older write-ups that call MongoDB the only database, describe Payload 2.
Payload 2 collection files imported CollectionConfig from payload/types and ran on Express. Payload 3 imports the type from payload.
import type { CollectionConfig } from 'payload'
export const Articles: CollectionConfig = {
slug: 'articles',
admin: {
useAsTitle: 'title',
},
access: {
read: () => true,
create: ({ req: { user } }) => Boolean(user),
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'content', type: 'richText' },
{ name: 'publishedDate', type: 'date' },
],
}
The collection is registered in payload.config.ts through buildConfig, along with secret and one db adapter. Rich text needs @payloadcms/richtext-lexical. Image resizing needs sharp. GraphQL needs the graphql package. With that package installed, the API is at /api/graphql and can be turned off with graphQL.disable in the config, or per collection with graphQL: false. The documented auth mutations cover login, logout, refresh, forgot-password, reset-password, unlock, and verify. They do not include a two-factor mutation.
There is no official payloadcms/payload runtime image. You containerize the Next.js app you generated. Payload’s new-project page at payloadcms.com/new says project creation is only available for Enterprise teams. The get-started page says the open-source app is self-hosted, and that Enterprise adds hosting, SSO, and visual editing. This page has no public self-serve Cloud price to quote.
APIs, databases, and access
| Platform | REST | GraphQL | Same-process API | WebSockets |
|---|---|---|---|---|
| Strapi 5 | Content API on the Strapi process | @strapi/plugin-graphql, route /graphql, subscriptions off in the default plugin config |
No | No |
| Directus 12 | /items/:collection |
Generated with REST | No | Yes, after WEBSOCKETS_ENABLED=true on self-host |
| Payload 3 | Generated with the app | /api/graphql once the graphql package is installed |
Local API | No content socket in the documented API |
| Platform | PostgreSQL | MySQL / MariaDB | SQLite | MongoDB | Other SQL |
|---|---|---|---|---|---|
| Strapi 5 | Yes | Yes | Yes | No, since v4 | No |
| Directus 12 | Yes | Yes | Yes | No | MS SQL, Oracle, CockroachDB |
| Payload 3 | Yes, stable adapter | No | Yes | Yes | No |
| Topic | Strapi 5 | Directus 12 | Payload 3 |
|---|---|---|---|
| License of the core you self-host | Community edition, MIT | MSCL 1.0, source-available | MIT |
| SSO | Growth add-on or Enterprise | Not on Core. Team, Enterprise, or the Grant | Enterprise, per the get-started page |
| Role or access rules | Community includes RBAC | Custom permission rules are ignored on unlicensed Core | Function-based access in the config |
| API tokens | Yes | Yes | Yes |
| 2FA | Not in the Users and Permissions docs. Strapi’s blog shows a custom OTP extension | Require-2FA policy | No 2FA mutation in the documented GraphQL auth operations |
| Audit logs and review workflows | Enterprise on the CMS pricing page | Not part of the Core feature list used here | Enterprise, alongside SSO, on the get-started page |
A client that needs its own query shape can sit in front of any of these APIs. The GraphQL backend-for-frontend with Apollo Server is that pattern, independent of which CMS holds the content.
Self-hosting and the hosted prices
Strapi Cloud, from the cloud pricing page, is a separate bill from the CMS license:
| Plan | Price | API requests included | Notes from the same page |
|---|---|---|---|
| Starter | $35 per project / month | 100,000 | Sleeps when idle. 50 GB asset storage, 50 GB asset bandwidth |
| Pro | $90 per project / month | 1,000,000 | Always on. Weekly backups |
| Business | $450 per project / month | 10,000,000 | Daily backups, 99.9% uptime SLA, two environments |
Extra API requests are listed at $1.50 per 25,000. Prices exclude tax.
Directus’s pricing page shows two different Team prices. The plan cards list Core at $89 per month billed yearly, or $99 monthly, with 3 seats, 25 collections, and 5 flows, and Team at $588 per month billed yearly, or $698 monthly, with 10 SSO seats, 50 collections, and 20 flows. The FAQ on that same page says Team is $499 per month on an annual commitment ($5,988 per year) or $599 per month monthly, and that Directus Cloud adds $89 per month billed yearly, or $99 monthly, on top of Core and Team. Use the live page before you budget. SSO is not on Core, and extra Team seats are listed at $50 per seat per month.
Payload’s self-host cost is the Node host, the database, and file storage. There is no MIT-edition seat fee on the pages checked. New Payload Cloud projects are limited to Enterprise, so this page does not quote a Cloud tier.
The static site in front of the CMS is its own pipeline. Netlify for Hugo is one managed path, including the Vercel and Cloudflare Pages alternatives named on that page. Deploying Hugo to S3 is the object-storage path. After the HTML is public, IndexNow can tell participating engines about the new URLs. None of the three CMS products submits those URLs for you.
Which one to run
Strapi 5 fits a team that wants a separate Node CMS, a content-type UI, and the MIT Community edition, and that will create the database inside Strapi. Growth or Enterprise is a second purchase when the project needs Live Preview, SSO, review workflows, or audit logs.
Directus 12 fits a SQL database that already exists, and a Studio other people will use without editing TypeScript. Read the MSCL terms and the Core limits before you depend on SSO, custom permission rules, or a self-hosted LLM. The Open Innovation Grant is the path that removes those limits for organizations under the revenue and headcount thresholds.
Payload 3 fits a Next.js codebase that wants the schema in Git and the Local API in the same process. Plan on the Node and Next ranges in the installation doc. SSO and the hosted project flow are Enterprise. PostgreSQL is a normal adapter, and payload/types is the Payload 2 import.
What older tutorials still get wrong
| Older claim | What the current docs say |
|---|---|
| All three are MIT | Payload’s core and Strapi Community are MIT. Directus 12 is MSCL |
| Strapi supports MongoDB | Removed in v4. Strapi 5 is PostgreSQL, MySQL, MariaDB, and SQLite |
| Payload defaults to MongoDB, Postgres is beta, no GraphQL | Payload 3 ships Postgres, SQLite, and MongoDB adapters. GraphQL is at /api/graphql when the graphql package is installed |
import { CollectionConfig } from 'payload/types' |
Import the type from payload |
One Compose service image covers all three: strapi/strapi, directus/directus, payloadcms/payload |
Only directus/directus is an official image. Strapi says to build your own. Payload is the Next.js app |
Strapi --quickstart |
Deprecated in Strapi 5. Use npx create-strapi@latest and --non-interactive |
| Payload Cloud is a public beta with pricing still to come | payloadcms.com/new limits new project creation to Enterprise teams |
| A single response-time table ranks the three | No shared published benchmark was found. This page does not rank the three on milliseconds |
Conclusion
Strapi 5, Directus 12, and Payload 3 still answer different layouts of the same publishing problem. Strapi is a Node service that owns its SQL schema. Directus is the API and Studio on a SQL database you already run, under a source-available license with a Core tier. Payload is the CMS inside the Next.js app, MIT-licensed, with the database behind one adapter. The frontend deploy, the CDN, and the indexing ping stay outside all three.
When content has to answer agents
A comparison of Strapi, Directus, and Payload is framed around architecture, the API, the admin UI, and self-hosting, which is the right frame for classic headless work. That frame stretches when the same content also has to feed AI agents, retrieval pipelines, and channels that were not on the roadmap when the schema was designed. Sanity sits in that adjacent category. The project describes the platform as a content operating system for the AI era, built around structured content that any consumer, human or machine, can query.
Content Lake stores JSON documents, and every field is defined by schema-as-code in TypeScript, in the Studio configuration, rather than as a database constraint. GROQ queries that content directly, so a Next.js page, a React Native app, and an agent all pull from the same source without a sync layer in between. Sanity Studio is a customizable React workspace editors shape to their own workflow. The Agent API lets agents read and write against the same typed schema developers already maintain, and Agent Actions run schema-aware automations on that content.
The practical consequence for a team evaluating Strapi, Directus, or Payload is a second question alongside API and developer-experience fit: what happens when an AI agent needs a governable, typed answer from the content. Teams that expect that requirement within the lifetime of the CMS decision tend to weigh this hosted content layer as part of the same choice.
Useful Links
Official docs
- Strapi 5 documentation
- Strapi database configuration
- Strapi Docker guide
- Directus documentation
- Directus Docker guide
- Directus licensing overview
- Payload documentation
- Payload installation