deployed at the edge · 300+ locations

Content,
decentralized
by design.

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.

0ms
cold starts
publish targets
page versions
0xcms — wrangler@edge

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

The protocol layer for your content

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.

🔐

OAuth 2.1 + PKCE

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.

⛓️

Pluggable publish targets

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.

🧬

Versioned everything

Every save mints a new page version. Roll back any page to any point in its history — your content ledger is append-only.

🛰️

Realtime page sync

A Durable Object coordinates concurrent editors per page, so your team never overwrites each other's work.

🗄️

Private media vault

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.

🧩

Worker plugins

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.

See the full feature list

19 categories · read straight from the source

HARDENED BY DEFAULT: CSP nonces Rate limiting Cross-origin mutation guard Canonical host enforcement Capability-based access

// 02 — architecture

Publish flow, on-chain style

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.

DB · private
users · sessions
draft_pages · page_versions
tags · trash · media_files
— publish ⟶ un-publish
d1 · PUBLISHED_DB
live_pages · live_page_tags
r2 · static JSON
pages/<uuid>.json · index.json
plugin · your Worker
IPFS · webhooks · search index
— reads ⟶ edge cache
🌐 Public site
Liquid templates
served from 300+
edge locations

PUBLISH_TARGETS = "d1,r2" · plugins opt in with publishTarget: true · per-target failures reported, never silent

Hono + TypeScript
router & runtime
Tailwind + VanillaJS
zero-framework admin UI
LiquidJS + Emmet
templating & authoring

// 03 — get started

Mint your own instance

From clone to edge deployment in ten steps. All you need is a Cloudflare account and wrangler.

00

Install 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
01

Clone the repo & install dependencies

Grab the source from LionRockJS/worker-cms, then install.

$ git clone https://github.com/LionRockJS/worker-cms.git
$ cd worker-cms
$ npm install
02

Create the D1 databases

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
03

Run migrations

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
04

Create the private R2 media vault and job queue

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
05

Configure secrets

⚠️ 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
06

Enable OAuth providers

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

GitHub — scopes: read:user, user:email

  1. Go to github.com/settings/developersOAuth AppsNew OAuth App. (For an org-owned app, use Settings → Developer settings under the org instead.)
  2. Set Homepage URL to your domain and Authorization callback URL to https://<your-domain>/auth/callback.
  3. Click Register application. Copy the Client ID.
  4. Click Generate a new client secret and copy it immediately — GitHub shows it only once.

Google — scopes: openid, email, profile

  1. Open console.cloud.google.com/apis/credentials and select (or create) a project.
  2. Configure the OAuth consent screen first — pick External, fill in app name + support email. Add your email under Test users while the app is unpublished.
  3. Go to CredentialsCreate CredentialsOAuth client ID → application type Web application.
  4. Under Authorized redirect URIs add https://<your-domain>/auth/callback (and the localhost URI for dev).
  5. Click Create, then copy the 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.

07

Run locally

$ npm run dev
# → http://localhost:8787
08

Ship to the edge 🚀

$ npm run deploy
09

Sign in, then grant yourself admin

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]'"
++

Optional: fan out to more publish targets

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-manageRegister 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

Don't trust.
Verify it yourself.

The live instance is running on the edge right now.

Launch 0xCMS ↗

cms.eventuai.com