Preview a web application·续
relay(server, upstream); relay(upstream, server);
// Pass on the subprotocol that the server chose. A browser closes a // connection that requested one and receives none. const headers = new Headers(); const protocol = response.headers.get("Sec-WebSocket-Protocol"); if (protocol) { headers.set("Sec-WebSocket-Protocol", protocol); }
return new Response(null, { status: 101, webSocket: client, headers }); } ```
src/index.tsts
function bridge(response: Response): Response {
const upstream = response.webSocket!;
const [client, server] = Object.values(new WebSocketPair());
// Receive binary messages as `ArrayBuffer` instead of `Blob`
upstream.binaryType = "arraybuffer";
server.binaryType = "arraybuffer";
upstream.accept();
server.accept();
// A listener that throws drops the connection, so these never throw.
const relay = (from: WebSocket, to: WebSocket) => {
from.addEventListener("message", (event) => {
try {
to.send(event.data);
} catch {
from.close(1011, "The other side closed");
}
});
from.addEventListener("close", (event) => {
// 1005 and 1006 report a missing or abnormal close and cannot be sent.
const code =
event.code === 1005 || event.code === 1006 ? 1000 : event.code;
try {
to.close(code, event.reason);
} catch {}
});
};
relay(server, upstream);
relay(upstream, server);
// Pass on the subprotocol that the server chose. A browser closes a
// connection that requested one and receives none.
const headers = new Headers();
const protocol = response.headers.get("Sec-WebSocket-Protocol");
if (protocol) {
headers.set("Sec-WebSocket-Protocol", protocol);
}
return new Response(null, { status: 101, webSocket: client, headers });
}
```
2. In the `fetch()` handler of your Durable Object, bridge responses that open a WebSocket:
*src/index.tsts*
```ts
try {
const response = await container.getTcpPort(port).fetch(request);
return response.webSocket ? bridge(response) : response;
} catch {
```
With the bridge, the sandbox keeps running while any page has a connection open, even when no messages arrive. After the last connection closes, the inactivity timeout applies again.
## Use hot reloading with a development server
Development servers such as [Vite ↗︎](https://vite.dev/) reload the page over a WebSocket. With the bridge from the previous section, the reload connection reaches the server. Two server settings decide whether hot reloading works:
- The server checks the `Host` header of each request. Vite rejects hostnames that it does not know, so add the preview hostname to [`server.allowedHosts` ↗︎](https://vite.dev/config/server-options#server-allowedhosts). Under `/previews/<NAME>/`, requests arrive with the `Host` header `container`.
- The server builds absolute URLs for its assets and for the reload connection from its base path. Under `/previews/<NAME>/`, those URLs leave the preview. [Serve each preview on its own hostname](https://developers.cloudflare.com/sandbox/previews/serve-previews-on-their-own-hostnames/), where paths reach the server unchanged.
The following Vite configuration accepts requests for previews on their own hostnames under `example-previews.com`:
*vite.config.jsjs*
```js
import { defineConfig } from "vite";
export default defineConfig({
server: {
host: "0.0.0.0",
port: 8080,
allowedHosts: [".example-previews.com"],
},
});
Share a preview with a link
Give someone access to one preview for a limited time. In this example, the Durable Object stores random tokens with an expiry time. The Worker forwards requests under /shared/<NAME>/<TOKEN>/ only while the token is valid.
- Add methods that create and check tokens to your Durable Object:
src/index.tsts
export class MyContainer extends DurableObject<Env> {
// ...
async share(ttl: number): Promise<string> {
const token = crypto.randomUUID();
await this.ctx.storage.put(`share:${token}`, Date.now() + ttl);
return token;
}
async isShared(token: string): Promise<boolean> {
const key = `share:${token}`;
const expires = await this.ctx.storage.get<number>(key);
if (expires !== undefined && expires <= Date.now()) {
await this.ctx.storage.delete(key);
return false;
}
return expires !== undefined;
}
}
```
2. In the `fetch()` handler of your Worker, add routes that create a link and serve shared previews, before the `404` response:
*src/index.tsts*
```ts
const share = /^\/shares\/([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)$/.exec(url.pathname);
if (share && request.method === "POST") {
const sandbox = env.MY_CONTAINER.getByName(share[1]);
const token = await sandbox.share(60 * 60 * 1000);
return Response.json({ url: `${url.origin}/shared/${share[1]}/${token}/` });
}
const shared = /^\/shared\/([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\/([^/]+)(\/.*)?$/.exec(url.pathname);
if (shared) {
const sandbox = env.MY_CONTAINER.getByName(shared[1]);
if (!(await sandbox.isShared(shared[2]))) {
return new Response("Not found", { status: 404 });
}
return forward(sandbox, request, shared[3]);
}
```
Authenticate the `POST` request, so that only the owner of a preview can create links to it. Code in a preview runs on this hostname and can send requests to this route with the visitor's cookies. To prevent that, serve the route from another hostname, or authenticate it with a credential that pages in the browser cannot send, such as a secret that your application backend adds.
The shared route needs no authentication, because the token is the permission.
3. Deploy your Worker, then create a link to the preview named `ada`:
```sh
curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/shares/ada --request POST
```
```json
{
"url": "https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/shared/ada/<TOKEN>/"
}
```
Open the URL in a browser. The preview loads for one hour. After that, and for any other token or preview name, the Worker responds with `404`.
Anyone with the link can open the preview until it expires. Each token belongs to one preview name. To revoke a link early, delete its key from Durable Object storage.
## Stop a preview
Stop the server when the preview is no longer needed, instead of waiting for the inactivity timeout:
1. Add a method that stops the container to your Durable Object:
*src/index.tsts*
```ts
export class MyContainer extends DurableObject<Env> {
// ...
async stop(): Promise<void> {
if (this.ctx.container?.running) {
await this.ctx.container.destroy();
}
this.ready = false;
}
}
```
2. In your Worker, call it for `DELETE /previews/<NAME>`, before forwarding:
*src/index.tsts*
```ts
if (!preview[2] && request.method === "DELETE") {
await sandbox.stop();
return new Response(null, { status: 204 });
}
```
3. Deploy your Worker, and stop the preview named `ada`:
```sh
curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/previews/ada --request DELETE
```
The Worker responds with `204`. The next request to the preview starts a new server, with an uptime of `0`.
## Related resources
- [Serve previews on their own hostnames](https://developers.cloudflare.com/sandbox/previews/serve-previews-on-their-own-hostnames/)
- [Sandbox lifetime](https://developers.cloudflare.com/sandbox/concepts/lifetime/)
- [Sandbox security](https://developers.cloudflare.com/sandbox/concepts/security/)
- [`getTcpPort()`](https://developers.cloudflare.com/containers/api/durable-object-container/#gettcpport)
Was this helpful?
YesNo
## On this page
[Docs](https://developers.cloudflare.com/)
```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/sandbox/previews/#page","headline":"Preview a web application","description":"Open a web server that runs in a Linux sandbox through your Worker, keep its WebSocket connections open, and share it with a link.","url":"https://developers.cloudflare.com/sandbox/previews/","inLanguage":"en","image":"https://developers.cloudflare.com/sandbox/previews/og.png?v=a671b00fd45ffa2b","dateModified":"2026-10-22","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Give each sandbox preview its own hostname, so code in one preview cannot use your application cookies or the storage of another preview. title: Serve previews on their own hostnames image: https://developers.cloudflare.com/sandbox/previews/serve-previews-on-their-own-hostnames/og.png?v=052e5363148687ab
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/sandbox/llms.txt
Use this file to discover all available pages before exploring further.
