Server sites (Next.js / OpenNext) on <slug>.devstack.bid
On this page
Static builds are stored in R2 and served by devstack-sites. Apps that need a server
(Next.js with API routes, SSR, middleware) are deployed as their own Worker per project.
Workers for Platforms (dispatch namespaces) is not enabled on the account (API error 10121),
so the fallback below is used. If WfP is bought later, deployServerWorker can target a
dispatch namespace and devstack-sites can use a dispatcher binding instead of the proxy.
Flow#
- Build container (
container/build-server.mjs) runs install + build. For Next.js the build command isnpx opennextjs-cloudflare build, output.open-next. - If the output has
.open-next/worker.js(and noindex.html), the container runswrangler deploy --dry-run --outdirto bundle it into one ES module. No Cloudflare credentials are used or present in the container. Artifact layout:server.json,worker/worker.js,assets/**. - The executor (
src/lib/build/executor.ts) seesserver.jsonand callsdeployServerWorker(src/lib/hosting/server-worker.ts): owner-tag check → assets upload session → asset buckets →PUT /workers/scripts/<name>(guard module + customer module, ASSETS +DEVSTACK_PROXY_KEYbindings, owner tag) → enable workers.dev.<name>issite-<sha256(project id)[0:20]>for production andsite-<…>-<sha256(deployment id)[0:12]>for previews, so previews never replace production and a re-used slug can never overwrite another project's Worker. publishServerwriteshosts/<label>.jsonin R2 with{ projectId, deploymentId, worker }for the preview host and (production) the<slug>host. Ownership rules are unchanged: the first project to claim a host owns it.devstack-sitesreads the host record; if it has a validworkerit proxies the request (any method, path, query, body; same-origin redirects rewritten) tohttps://<name>.<WORKERS_SUBDOMAIN>.workers.dev. Only names matching^site-[a-f0-9]{20}(-[a-f0-9]{12})?$and not in the platform denylist are proxied.- Request headers: allowlist only (
FORWARD_REQUEST_HEADERS);Cookie,AuthorizationandCF-*never reach customer code.X-Forwarded-Host/ProtoandX-Devstack-Proxyare added. - Response: every
Set-Cookieis removed (server sites are cookie-less for now), platform security headers are applied (customer CSP kept, ourframe-ancestorsCSP added), the report badge is injected into HTML, and workers.dev 404/5xx not markedx-devstack-origin: projectbecome our "Site temporarily unavailable" page. The proxy usesglobal_fetch_strictly_public.
- Request headers: allowlist only (
Per-site zone routes were tried first; the deploy token has no Workers Routes permission on
the devstack.bid zone, and proxying avoids one route per customer.
Configuration (platform Worker nvx-stack-builder)#
| Name | Kind | Purpose |
|---|---|---|
SITES_CF_API_TOKEN |
secret | Account token with Workers Scripts: Edit only. Not configured yet → server builds fail with invalid_config (honest Blocked, never faked). |
SITES_CF_ACCOUNT_ID |
var/secret | Account id for that token. |
SITES_PROXY_SECRET |
secret on both Workers | 32-byte random master; per-worker key = HMAC-SHA256(master, name). Generated at deploy, never printed. Server builds stay Blocked without it. |
WORKERS_SUBDOMAIN |
var on devstack-sites |
examplessdk (the account workers.dev subdomain). |
Limits / security#
- Worker bundle ≤ 10 MiB uncompressed (
MAX_WORKER_BYTES).server.jsonis ignored: flags are alwaysnodejs_compat+global_fetch_strictly_public, date pinned (SERVER_COMPAT_DATE). - Customer Workers get no bindings except their own
ASSETS; they cannot reach D1, R2 or platform secrets. They run on the shared account, so customer code shares account-level limits; WfP would add per-tenant isolation (recommended before opening this widely). - Direct
workers.devhits are answered by the guard with 404 unless they carry the proxy key. Exception: static files served by the ASSETS binding are matched before any code runs, so public assets (/_next/static/…) are also reachable directly. They are public anyway. - Preview Workers are cleaned up by the 5-minute cron (
cleanupPreviewWorkers). A preview Worker is deleted only if it is NOT one of its project's newest 5 (always kept) AND older than 14 days AND its name matchessite-<20hex>-<12hex>AND its owner tag's project id hashes to the name prefix. Host aliases pointing at it (tracked in R2workers/<name>.json) are removed; a preview URL whose Worker is gone shows "This preview has expired" (410). - Proxied HTML gets the badge via HTMLRewriter (inside
<body>, or at document end when there is no body; also forapplication/xhtml+xml).Content-Encodingis dropped on rewritten bodies. - The proxy target host is fixed; the request path can never change it (see tests "N1 SSRF").
- Next action (before opening server sites widely): move customer Workers to a separate Cloudflare account or Workers for Platforms dispatch namespace; delete preview Workers after N days.
Build reliability fixes in 0.11.0#
executor_lostnow records the Containers runtime reason (Build container did not start (HTTP 500): Failed to start container: …).- The 5-minute cron re-delivers queued container jobs that were never claimed (lost queue
messages); duplicate delivery is a no-op because only
queuedrows are claimed. - OpenNext projects used to fail at
missing_indexbecause the container requiredindex.html; they now take the server path.
Source: docs/platform/SERVER-SITES.md · /docs/platform/server-sites.md