Open-source headless CMS

A headless CMS you assemble from connectors.

Pick a connector for each layer, the API runtime, your database, object storage, cache, and where the admin is hosted. Then run the same code self-hosted, in Docker, or on Cloudflare's edge. Content types are data, so adding a field never needs a migration.

// ANY PROVIDERRuntimeBunBunCloudflare WorkersWorkersDockerDockerDataSQLiteSQLiteSupabaseSupabasePlanetScalePlanetScaleObject storageCloudflareR2AWS S3AWS S3MinIOMinIOCacheCloudflareKVRedisRedisHostingCloudflare WorkersWorkers assetsVercelVercelStatic hostStatic hostOpenCMSv0 · ALPHASCHEMA AS DATACONFORMANCE-TESTEDSAME CODE, ANY TARGET// ONE INTERFACEONE UNIFIED INTERFACEREST Admin APISTABLEGET/api/entriesPOST/api/entriesGET/api/content-typesAdmin UISHIPPEDReact · same origin · no CORSMCP surfaceSOONtools for agents · same servicesone connector per layer
// The core idea

Two simple ideas run through everything.

01

Every layer is a connector.

The API runtime, your database, object storage, cache, and hosting each sit behind one small interface. Write a connector for your provider and nothing else in the CMS changes. And a connector is only done when it passes the shared test suite, so you never have to trust a README.

02

Content types are data, not code.

Define and edit content types from the admin while the app runs. Entries live in one table with their fields in a JSON column, validated on the way in. Adding a field needs no migration and no deploy, the database shape never changes.

opencms.config.ts
// one config, one connector per layer.
export default defineConfig({
  runtime: cloudflareWorkers(),
  data:    supabase({ url: env.SUPABASE_URL }),
  storage: r2({ bucket: "media" }),
  cache:   kv(env.CACHE),
  hosting: workersAssets(),
})
// swap any line for a different provider. same CMS.
// Multi-connector

Five layers. Pick a connector for each.

OpenCMS keeps the CMS separate from the infrastructure it runs on. Choose a provider for each layer and watch the config assemble itself, one line per layer. See where it runs →

Runtime connector
Runtimethe API

Serves the REST Admin API.

Data connector
Datayour database

Where content types and entries are stored.

Object storage connector
Object storageany S3-compatible

Media and file uploads, in any S3 bucket.

Cache connector
CacheKV

Fast key-value reads close to your users.

Hosting connector
Hostingthe admin UI

Where the dashboard is served from.

opencms.config.ts
import { defineConfig } from '@opencms/core'

export default defineConfig({
runtime: cloudflareWorkers(),
data: d1(env.DB),
storage: r2({ bucket: "media" }), // planned
cache: kv(env.CACHE), // planned
hosting: workersAssets(),
})
3 of 5 available todaythe rest are planned
// Runs anywhere

Self-host it, containerize it, or ship it to the edge.

The same TypeScript runs in every target. Only the connector bindings change, and the admin ships at the same origin so sessions work with no CORS.

Self-hosted
runtimeBun server
dataSQLite
adminserved by the API
DockerSoon
imageone container
dataSQLite or Postgres
hostanywhere it runs
Edge
runtimeCF Workers
dataD1 · R2 · KV
adminWorkers assets
// What you get

Small at the core, honest about its guarantees.

Migration-free modeling

Content types are data. Add a field with no schema migration and no deploy.

Bring your own backend

Cloudflare, Supabase, PlanetScale, SQLite, or any S3 bucket. One connector per layer.

Conformance-tested

A connector is done only when it passes the shared test suite. Run it yourself.

Auth built in

better-auth handles sessions and API keys. Two roles, and anonymous reads are published-only.

Agent-ready

Soon

An MCP surface so an agent runs the CMS through the same validated services a human uses.

One artifact to ship

API and admin ship at one origin. One deploy carries both, with no CORS to configure.

// Roadmap

Four milestones shipped, two to go.

M1Core engine, SQLite connector, test-kit, REST APIShipped
M2Cloudflare connectors (D1) and the Workers runtimeShipped
M3Auth: better-auth sessions, API keys, and RBACShipped
M4Admin UI in React, with a Playwright end-to-end suiteShipped
M5Media: an S3-compatible storage connector (R2, MinIO, AWS) and a media libraryPlanned
M6The MCP agent surface, then a Postgres connector and connector SDK docsPlanned
264 backend tests passing51 end-to-end scenariostypecheck clean5 connector layers

A CMS small at its core, portable by construction.