0xCMS is an edge-native content management system that runs entirely on Cloudflare Workers. OAuth 2.1 sign-in, publishing that fans out to any target — database, static JSON, IPFS — versioned content, private media vault. Zero servers, zero cold starts.
❯ npm run deploy
Compiled Tailwind admin UI ✓
Uploaded 0xcms (2.41 sec)
D1 bindings: DB · PUBLISHED_DB
R2 vault: MEDIA_BUCKET [private]
Publish targets: d1 · r2 · plugin:ipfs
Durable Object: PageSyncDO [realtime]
✔ Deployed to https://cms.eventuai.com
❯
// 01 — introduction
Everything a serious publishing stack needs, compiled into a single Worker. No origin servers. No databases to babysit. Private data is separated by default, with explicit trust boundaries for administrators and plugins.
Sign in with Eventuai, GitHub, Google, Microsoft, or Apple. Dual JWTs use 15-minute access tokens plus rotatable 7-day refresh sessions hashed in D1; revocation stops refresh immediately, while an issued access token expires naturally.
Publishing fans one snapshot out through adapters: the published D1 store, static JSON in an R2 bucket, or any plugin Worker — pin to IPFS, ping a webhook, feed a search index. Per-target failures surface in the editor.
Every save mints a new page version. Roll back any page to any point in its history — your content ledger is append-only.
A Durable Object coordinates concurrent editors per page, so your team never overwrites each other's work.
Uploads land in a private R2 bucket — never public. The Worker gates every read at /media/*, with on-the-fly image resizing served from /media-preview/* for thumbnails.
Extend with separate Workers registered over HTTPS: lifecycle hooks, custom content types, fields & blocks, admin pages, publish targets. No runtime installs, no CMS redeploy — paste a URL in the admin UI, each plugin gets its own secret.
19 categories · read straight from the source
// 02 — architecture
Drafts stay private. One publish action mints a snapshot and fans it out through adapters — to the published database, a static-JSON bucket, or any plugin Worker you bind. Readers never touch your admin data.
PUBLISH_TARGETS = "d1,r2" · plugins opt in with publishTarget: true · per-target failures reported, never silent
// 03 — get started
From clone to edge deployment in ten steps. All you need is a Cloudflare account and wrangler.
Install Cloudflare's Workers CLI globally, confirm it is available, then sign in to your Cloudflare account.
$ npm install -g wrangler
$ wrangler --version
$ wrangler login
Grab the source from LionRockJS/worker-cms, then install.
$ git clone https://github.com/LionRockJS/worker-cms.git
$ cd worker-cms
$ npm install
Two isolated stores: cms for private admin data, cms-published for live content only. Copy the printed database_id values into wrangler.toml.
$ npx wrangler d1 create cms
$ npx wrangler d1 create cms-published
Run the first pair locally, then repeat with --remote for production. Locally, npm run db:migrate applies both.
$ npx wrangler d1 migrations apply cms
$ npx wrangler d1 migrations apply cms-published
$ npx wrangler d1 migrations apply cms --remote
$ npx wrangler d1 migrations apply cms-published --remote
The bucket stays private; the Worker serves objects at /media/<key>. The MEDIA_BUCKET binding ships in wrangler.toml.
$ npx wrangler r2 bucket create worker-cms-media
$ npx wrangler queues create cms-admin-jobs
⚠️ The JWT secret must be at least 32 characters — the Worker refuses to serve in production with a weaker key. Create .dev.vars for local dev.
# random 32-byte signing key
$ openssl rand -hex 32 | npx wrangler secret put JWT_SECRET
Pick your gateways in wrangler.toml, then register an OAuth app with each provider to obtain a client ID + client secret. Every provider redirects back to the same callback — your OAUTH_REDIRECT_URI:
# production — must match wrangler.toml exactly
https://<your-domain>/auth/callback
# local dev (.dev.vars)
http://localhost:8787/auth/callback
https://<your-domain>/auth/callback.Client ID.https://<your-domain>/auth/callback (and the localhost URI for dev).Client ID and Client secret.List the gateways you registered and drop each client ID into wrangler.toml. Set CANONICAL_ORIGIN to your own domain too — the Worker enforces the canonical host and otherwise redirects to its default origin:
[vars]
ENABLED_PROVIDERS = "eventuai,github,google"
GITHUB_CLIENT_ID = "<github-client-id>"
GOOGLE_CLIENT_ID = "<google-client-id>"
OAUTH_REDIRECT_URI = "https://<your-domain>/auth/callback"
CANONICAL_ORIGIN = "https://<your-domain>"
Then push the matching client secrets as encrypted secrets (never commit them). Run each command and paste the value at the prompt:
$ npx wrangler secret put GITHUB_CLIENT_SECRET
$ npx wrangler secret put GOOGLE_CLIENT_SECRET
Enter secret values only at Wrangler's interactive prompt or from a protected file; do not put them directly in a shell command, wrangler.toml, or source control. Account, database, route, and OAuth client IDs are identifiers rather than bearer secrets.
$ npm run dev
# → http://localhost:8787
$ npm run deploy
Your account only exists after you log in. With the app running — locally or on the edge — open /auth/login and sign in once through any provider to create your user row, then promote it. Roles are a comma-separated list.
# production DB — after you signed in on the deployed site
$ npx wrangler d1 execute cms --remote \
--command "UPDATE users SET role='admin' WHERE email='[email protected]'"
Testing on localhost:8787? Drop --remote to hit the local DB, then sign out and back in so the new role lands in your token.
$ npx wrangler d1 execute cms \
--command "UPDATE users SET role='admin' WHERE email='[email protected]'"
Publishing defaults to the d1 target. Add r2 for static JSON snapshots in a bucket:
$ npx wrangler r2 bucket create worker-cms-published
# wrangler.toml — bind the bucket, then enable the target
[[r2_buckets]]
binding = "PUBLISH_BUCKET"
bucket_name = "worker-cms-published"
[vars]
PUBLISH_TARGETS = "d1,r2"
For everything else, publish targets are plugin Workers. Two are ready to deploy —
plugin-publish-ipfs
(pins pages to IPFS via Pinata) and
plugin-publish-webhook
(HMAC-signed webhooks for search indexers, rebuilds, deploy hooks) — or write your own: any Worker whose manifest declares publishTarget: true.
# 1 · deploy the plugin Worker and note its URL
$ git clone https://github.com/zeroxcms/plugin-publish-ipfs && cd plugin-publish-ipfs
$ npm install && npx wrangler deploy
Then in the CMS admin open /admin/plugins-manage → Register plugin and paste the Worker's URL. No CMS redeploy, no service bindings — the manifest (with publishTarget: true) is auto-discovered over HTTPS. The CMS mints a dedicated secret for that plugin; copy it from Plugins → Edit onto the plugin Worker, then redeploy:
# 2 · paste the per-plugin secret shown in Admin → Plugins → Edit
$ npx wrangler secret put PLUGIN_SECRET
$ npx wrangler deploy
Plugins are trusted application code. Proxied plugin pages and explicitly approved JavaScript run on the CMS origin, so review a plugin's source, grant only required page-type scopes, and rotate its dedicated secret if compromised.
// ready when you are
The live instance is running on the edge right now.
Launch 0xCMS ↗cms.eventuai.com