Cloudflare Workers and D1
Run the API at the edge, with content in D1.
The edge profile: the API runs on Cloudflare Workers, data lives in D1. Same code as the self-hosted Bun profile; only the connector binding differs. Auth (better-auth) uses the same D1 database; its tables are created on first request alongside the content schema.
Prerequisites
- A Cloudflare account (free tier is enough)
bun installdone at the repo root
1. Authenticate wrangler
cd apps/worker
bunx wrangler login2. Create the D1 database
bunx wrangler d1 create opencmsCopy the database_id from the output into apps/worker/wrangler.toml.
The schema needs no migration step: the Worker creates the fixed content
tables and the auth tables on first request (both inits are idempotent),
and content types never require DDL afterwards. Field indexes are created
at runtime by ensureIndexes.
3. Set the auth secret
openssl rand -base64 32 | bunx wrangler secret put BETTER_AUTH_SECRETOptionally set the canonical URL (used for cookies and origin checks) in
wrangler.toml once you know it:
[vars]
BETTER_AUTH_URL = "https://opencms-api.<account>.workers.dev"4. Build the admin and deploy
The Worker ships the admin SPA as static assets, so build it first:
bun run --cwd ../admin build
bunx wrangler deployWrangler prints your URL, e.g. https://opencms-api.<account>.workers.dev.
The admin UI is served at that URL's root; /api/* and /health hit the
Worker directly (run_worker_first).
5. Bootstrap the admin and smoke test
Open the deployed URL: the admin UI shows the first-run setup screen and creates the admin account there. Or bootstrap over HTTP:
API=https://opencms-api.<account>.workers.dev
curl $API/health
# 1. Bootstrap: the first user is the admin
curl -X POST $API/api/auth/sign-up/email \
-H 'content-type: application/json' \
-c cookies.txt \
-d '{"email":"you@example.com","password":"a-strong-password","name":"You"}'
# 2. Create the schema (admin session)
curl -X POST $API/api/content-types \
-H 'content-type: application/json' \
-b cookies.txt \
-d '{"name":"article","label":"Article","fields":[{"name":"title","kind":"text","required":true},{"name":"views","kind":"number","indexed":true}]}'
# 3. Mint an API key for machines (admin session; Origin header required
# on auth endpoints when authenticating with cookies outside a browser)
curl -X POST $API/api/auth/api-key/create \
-H 'content-type: application/json' \
-H "Origin: $API" \
-b cookies.txt \
-d '{"name":"ci","metadata":{"role":"editor"}}'
# 4. Write content with the key
curl -X POST $API/api/content/article \
-H 'content-type: application/json' \
-H 'x-api-key: <key from step 3>' \
-d '{"status":"published","data":{"title":"Hello from the edge"}}'
# 5. Published content is readable without auth
curl "$API/api/content/article?sort=createdAt:desc"Local development against workerd
cd apps/worker
echo 'BETTER_AUTH_SECRET=local-dev-secret-never-deploy-0123456789' > .dev.vars
bunx wrangler devwrangler dev runs the Worker in workerd with a local D1. Note that the
test suite already exercises real workerd D1 via Miniflare
(packages/connector-d1/test/conformance.test.ts), so bun test at the
repo root is the primary verification loop; wrangler dev is for manual
poking.
Access model (since M3)
- Anonymous requests can read published entries only.
- Editors (sessions or API keys with the editor role) manage content.
- Admins additionally manage content types, users and API keys.
- User accounts are created by admins (
POST /api/auth/admin/create-user); public signup only ever works for the very first user.
Caveats (current milestone)
- One Worker, one D1 database. Multi-tenant setups deploy one Worker per site for now.
- Media (R2) arrives in M5.