Serve each sandbox preview from its own hostname, such as ada.example-previews.com. Code in a preview runs in the visitor's browser, and each preview hostname is a separate origin. Code in one preview cannot read your application pages or the storage of another preview. Paths reach the server unchanged, so applications do not need a base path.
- .example-previews.com
- A wildcard DNS record and the route
*.example-previews.com/*send every preview hostname to your Worker. - ada
- Your Worker reads the sandbox name and calls
getByName("ada"). A name that is not a valid DNS label gets a404. - /app/
- The path reaches the web server in the container unchanged, with the preview hostname in the
Hostheader.
- A Durable Object whose
fetch()handler forwards requests to a web server in its container. To build one, refer to Preview a web application. - A domain for previews on Cloudflare, such as
example-previews.com, with Cloudflare managing its DNS. Use a domain that your application does not use.
-
In the DNS settings of the preview domain, add a proxied wildcard record. For example, add an
AAAArecord with the name*and the content100::, with the proxy status Proxied.The record lets Cloudflare receive requests for every preview hostname. The Worker route handles those requests, so Cloudflare never contacts the address in the record.
-
In
wrangler.jsonc, add the preview domain as a variable and route its subdomains to your Worker. Replaceexample-previews.comwith your preview domain:{ "vars": { "PREVIEW_DOMAIN": "example-previews.com", }, "routes": [ { "pattern": "*.example-previews.com/*", "zone_name": "example-previews.com", }, ], "workers_dev": true, }workers_dev = true [vars] PREVIEW_DOMAIN = "example-previews.com" [[routes]] pattern = "*.example-previews.com/*" zone_name = "example-previews.com"Wrangler turns off the
workers.devURL whenroutesis set.workers_dev: truekeeps it for requests that manage previews. -
Generate types for the new variable:
npx wrangler typesyarn wrangler typespnpm wrangler types -
In the
fetch()handler of your Worker, send each preview hostname to the Durable Object with the same name:src/index.jsjs export default { async fetch(request, env) { const url = new URL(request.url); const suffix = `.${env.PREVIEW_DOMAIN}`; // A preview hostname reaches only the sandbox with the same name. if (url.hostname.endsWith(suffix)) { const name = url.hostname.slice(0, -suffix.length); // Accept only sandbox names that are valid DNS labels if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name)) { return new Response("Not found", { status: 404 }); } // Forward with the `http:` scheme, because `getTcpPort().fetch()` // does not accept `https:` URLs url.protocol = "http:"; return env.MY_CONTAINER.getByName(name).fetch(new Request(url, request)); } // Other hostnames manage previews. return new Response("Not found", { status: 404 }); }, };src/index.tsts export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); const suffix = `.${env.PREVIEW_DOMAIN}`; // A preview hostname reaches only the sandbox with the same name. if (url.hostname.endsWith(suffix)) { const name = url.hostname.slice(0, -suffix.length); // Accept only sandbox names that are valid DNS labels if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name)) { return new Response("Not found", { status: 404 }); } // Forward with the `http:` scheme, because `getTcpPort().fetch()` // does not accept `https:` URLs url.protocol = "http:"; return env.MY_CONTAINER.getByName(name).fetch(new Request(url, request)); } // Other hostnames manage previews. return new Response("Not found", { status: 404 }); }, } satisfies ExportedHandler<Env>;The server receives the full path and the preview hostname in the
Hostheader.Every request to a preview hostname goes to the preview. Keep routes that manage previews, such as stopping one, on other hostnames, so code in a preview cannot call them from its own origin. Authenticate preview visitors on the preview hostname, and do not send your application session cookie there.
-
Deploy your Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
Open the preview named
adain your browser, and store a value in its browser console. Replaceexample-previews.comwith your preview domain:https://ada.example-previews.com/localStorage.setItem("name", "ada"); -
Open the preview named
grace, and read the value in its browser console:https://grace.example-previews.com/localStorage.getItem("name");The console prints
null. Each preview hostname is a separate origin, sogracecannot read the storage ofada.
Universal SSL covers a domain and its first-level subdomains, such as ada.example-previews.com. It does not cover deeper hostnames such as ada.previews.example.com. To serve previews under a subdomain, add Total TLS or an advanced certificate for it.
Previews under a domain that your application also uses share cookies with your application. Browsers send a cookie set with Domain=example.com to every subdomain, including every preview under previews.example.com.
Code in one preview can set a cookie with Domain=example-previews.com, and every other preview then receives that cookie. Set cookies on preview hostnames without a Domain attribute, and do not trust a cookie because it arrived on a preview hostname.
- Preview a web application
- Preview workspace example ↗︎: a deployable Worker that runs a Vite development server on a separate hostname for each sandbox.
- Sandbox security
- Routes