Static site control-plane that uploads build artifacts to Cloudflare R2, maps hostnames via Workers KV, and serves traffic through a Cloudflare Worker.
- Node.js 18 or later.
- Cloudflare account with:
- R2 bucket for site assets.
- Workers KV namespace for host → version mappings.
- API Token with Workers KV Storage:Edit and R2 permissions.
wrangleris provided via devDependencies; authenticate withwrangler login.
- Copy
.env.exampleto.envand fill every field (see definitions below). - The control-plane server requires the
CF_*andR2_*values to be present before you runnpm run dev. - For Wrangler commands, comment out the
CF_ACCOUNT_ID/CF_API_TOKENlines first and authenticate withwrangler login. After login, restore them so the server can start. - Regenerate
worker/wrangler.toml(see Toml Generation) whenever you change anyWRANGLER_*,WORKER_NAME, or route variables.
CF_ACCOUNT_ID/CLOUDFLARE_ACCOUNT_ID: Cloudflare account that owns the KV namespace and R2 bucket.CF_API_TOKEN/CLOUDFLARE_API_TOKEN: Cloudflare API token with Workers KV edit + R2 permissions.CF_KV_NAMESPACE_ID: KV namespace ID used by the control plane.R2_ACCOUNT_ID: Account ID hosting the R2 bucket.R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY: R2 API credentials used by the control plane uploader.R2_BUCKET: Name of the R2 bucket that stores the deployed site files.R2_REGION: R2 region (typicallyauto).R2_BASE_URL: Optional override for the R2 endpoint; not required for the current code path.PUBLIC_BASE_DOMAIN: Default domain used when constructing preview URLs in logs.WRANGLER_ACCOUNT_ID: Account injected intoworker/wrangler.toml.WRANGLER_KV_NAMESPACE_ID: KV namespace binding for the Worker.WRANGLER_R2_BUCKET_NAME: R2 bucket binding name for the Worker.WRANGLER_ROUTE_PATTERN: Route pattern placed intoworker/wrangler.toml.WRANGLER_ZONE_NAME: Zone name paired with the route pattern.WORKER_NAME: Worker name written toworker/wrangler.toml.REDIRECT_DOMAIN: Worker runtime env; redirectswww.<domain>to the apex.ROUTE_PATTERN/ROUTE_ZONE_NAME: Worker runtime hints that mirror the Wrangler route (informational, not read by the Worker).
- Install deps:
npm install - Start the API:
npm run dev(listens onhttp://localhost:3000) - Bundle your static site as a zip (you can zip the entire build folder; the server auto-detects a single top-level directory and uses it as the content root).
- Publish a build:
curl -F tenant=<tenant> \ -F host=<host.example.com> \ -F archive=@dist.zip \ http://localhost:3000/publish
- Update an existing mapping:
curl -H "Content-Type: application/json" \ -d '{"host":"<host.example.com>","tenant":"<tenant>","version":"<build>"}' \ http://localhost:3000/mapping
- Unpublish a site (removes host mapping, keeps R2 files):
curl -X POST -H "Content-Type: application/json" \ -d '{"host":"<host.example.com>"}' \ http://localhost:3000/unpublish
POST /publish(multipart/form-data)- Fields:
tenant,host, optionalversion, optionalroot(defaults toindex.html) - File:
archive(zip containing the site root) - Response:
{ ok, uploaded, mapping, url }
- Fields:
POST /mapping(application/json)- Body:
{ host, tenant, version, root? }
- Body:
POST /unpublish(application/json)- Body:
{ host } - Response:
{ ok, host, mapping }(mapping is null if host wasn't mapped)
- Body:
- Ensure
CF_ACCOUNT_ID/CF_API_TOKENare commented out in.env, then runwrangler loginonce to authenticate. - Regenerate
worker/wrangler.toml(see below). - Preview locally:
npm run worker:dev - Deploy to Cloudflare:
npm run worker:deploy - After deployment, point DNS or Cloudflare routes to the Worker hostname/pattern.
worker/wrangler.toml is generated from worker/wrangler.toml.template via worker/generate-config.js.
npm run worker:configThe script reads .env, loads every variable into process.env, and substitutes any ${VAR_NAME} placeholders it finds in the template. Keep placeholders limited to the vars you actually set, and regenerate whenever those values change.
- Wrangler commands ignore
wrangler loginwhenCF_API_TOKENis present; comment those lines before running Wrangler, then re-enable them for the server.