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. Trustless by default — even your media bucket is private.

🔐

OAuth 2.1 + PKCE

Sign in with Eventuai, GitHub, or Google. Dual JWTs — 15-minute access tokens plus rotatable 7-day refresh tokens, hashed in D1 for instant revocation.

⛓️

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 for previews.

🧩

Worker plugins

Extend with separate Workers over service bindings: lifecycle hooks, custom content types, fields & blocks, admin pages, publish targets. No runtime installs — just bind and redeploy.

HARDENED BY DEFAULT: CSP nonces Rate limiting Cross-origin mutation guard Canonical host enforcement Role-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 nine steps. All you need is a Cloudflare account and wrangler.

01

Install dependencies

$ 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

Add --remote for production. Locally, npm run db:migrate applies both.

$ npx wrangler d1 migrations apply cms
$ npx wrangler d1 migrations apply cms-published
04

Create the private R2 media vault

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
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 add a client ID + secret per provider.

ENABLED_PROVIDERS = "eventuai,github,google"
GITHUB_CLIENT_ID  = "<client-id>"
$ npx wrangler secret put GITHUB_CLIENT_SECRET
07

Grant yourself admin

Sign in once, then promote your account. Roles are a comma-separated list.

$ npx wrangler d1 execute cms --remote \
    --command "UPDATE users SET role='admin' WHERE email='[email protected]'"
08

Run locally

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

Ship to the edge 🚀

$ npm run deploy
++

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.

# deploy the plugin Worker, share the CMS secret with it
$ git clone https://github.com/zeroxcms/plugin-publish-ipfs && cd plugin-publish-ipfs
$ npm install && npx wrangler secret put PLUGIN_SECRET
$ npx wrangler deploy
# CMS wrangler.toml — bind it; no PUBLISH_TARGETS change needed
[[services]]
binding = "PLUGIN_PUBLISH_IPFS"
service = "cms-plugin-publish-ipfs"

[vars]
PLUGINS = "PLUGIN_PUBLISH_IPFS"

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