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"],
    },
});

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.

  1. Add methods that create and check tokens to your Durable Object:

src/index.tsts

   export class MyContainer extends DurableObject&lt;Env> {
    // ...

async share(ttl: number): Promise&lt;string> {
        const token = crypto.randomUUID();
        await this.ctx.storage.put(`share:${token}`, Date.now() + ttl);
        return token;
    }

async isShared(token: string): Promise&lt;boolean> {
        const key = `share:${token}`;
        const expires = await this.ctx.storage.get&lt;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://&lt;YOUR_WORKER>.&lt;YOUR_SUBDOMAIN>.workers.dev/shares/ada --request POST
   ```

```json
   {
    "url": "https://&lt;YOUR_WORKER>.&lt;YOUR_SUBDOMAIN>.workers.dev/shared/ada/&lt;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&lt;Env> {
    // ...

async stop(): Promise&lt;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://&lt;YOUR_WORKER>.&lt;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

[![](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NiIgaGVpZ2h0PSIzMCIgZmlsbD0ibm9uZSIgdmlld0JveD0iMCAwIDY2IDMwIj48ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfNzQ2XzIpIj48cGF0aCBmaWxsPSIjZmY5OTEwIiBkPSJNNTMuMTIgMTMuMTI1YzYuOTk3IDAgMTIuNjY4IDUuNzA4IDEyLjY2OCAxMi43NSAwIDEuMjU1LS4xOCAyLjQ2OC0uNTE2IDMuNjE0YS43Mi43MiAwIDAgMS0uNjk0LjUxMUg0Ni45NTNhLjM3NS4zNzUgMCAwIDEtLjM0OC0uNTFsLjMzNy0uODgyYzEuMDc2LTIuODMzIDQuMTQ2LTUuMTU0IDYuOTg3LTUuMjlsNS4zODQtLjI3N2EuNTQzLjU0MyAwIDAgMCAuNTE0LS41NDQuNTQzLjU0MyAwIDAgMC0uNTEtLjU0NGwtNS4wNTYtLjI5OWMtMi43MTgtLjE0LTQuMDQ2LTIuNTYtMy4zMTUtNS4wODFsLjg0Mi0yLjkwMmEuNzIuNzIgMCAwIDEgLjY0Ni0uNTI3cS4zNC0uMDIuNjg2LS4wMTkiLz48cGF0aCBmaWxsPSIjZmY1ZjA4IiBkPSJNMzUuNjA5IDBjNy4yOTUgMCAxMy40MjQgNS4wMjUgMTUuMTU3IDExLjgyNC4wNzMuMjg1LjA1LjU4Ni0uMDU0Ljg2MmwtMS4zODggMy42NzdjLTEuMDc2IDIuODMzLTMuODQyIDUuMjktNi45NyA1LjI5bC0yNC4xMDUuMjc4YS41MzMuNTMzIDAgMCAwLS41MjUuNTM1YzAgLjI5My4yMzQuNTMxLjUyNC41MzVsMjMuNzY1LjMxNmMyLjcyNCAwIDQuMDQ2IDIuNTYgMy4zMTYgNS4wODFsLS4zMDcgMS4wNjFhLjc1Ljc1IDAgMCAxLS43MTYuNTQxSC45NjRhLjcxLjcxIDAgMCAxLS43MTgtLjY0NiAxMSAxMSAwIDAgMS0uMDM0LS44NTRjMC01LjQ3NiA0LjE2Ni05Ljk3MyA5LjQ4Mi0xMC40NTZhNy4yIDcuMiAwIDAgMS0uMTY3LTEuNTQ0YzAtMy45MzUgMy4xNy03LjEyNSA3LjA4LTcuMTI1IDEuNTU1IDAgMi45OTQuNTA2IDQuMTYyIDEuMzYyQzIyLjg0OSA0LjQ5NyAyOC43MDcgMCAzNS42MDkgMCIvPjxwYXRoIGZpbGw9IiNmZjk5MTAiIGQ9Ik01My4xMiAxMy4xMjVjNi45OTcgMCAxMi42NjggNS43MDggMTIuNjY4IDEyLjc1IDAgMS4yNTUtLjE4IDIuNDY4LS41MTYgMy42MTRhLjcyLjcyIDAgMCAxLS42OTQuNTExSDQ2Ljk1M2EuMzc1LjM3NSAwIDAgMS0uMzQ4LS41MWwuMzM3LS44ODJjMS4wNzYtMi44MzMgNC4xNDYtNS4xNTQgNi45ODctNS4yOWw1LjM4NC0uMjc3YS41NDMuNTQzIDAgMCAwIC41MTQtLjU0NC41NDMuNTQzIDAgMCAwLS41MS0uNTQ0bC01LjA1Ni0uMjk5Yy0yLjcxOC0uMTQtNC4wNDYtMi41Ni0zLjMxNS01LjA4MWwuODQyLTIuOTAyYS43Mi43MiAwIDAgMSAuNjQ2LS41MjdxLjM0LS4wMi42ODYtLjAxOSIvPjxwYXRoIGZpbGw9IiNmZjVmMDgiIGQ9Ik0zNS42MDkgMGM3LjI5NSAwIDEzLjQyNCA1LjAyNSAxNS4xNTcgMTEuODI0LjA3My4yODUuMDUuNTg2LS4wNTQuODYybC0xLjM4OCAzLjY3N2MtMS4wNzYgMi44MzMtMy44NDIgNS4yOS02Ljk3IDUuMjlsLTI0LjEwNS4yNzhhLjUzMy41MzMgMCAwIDAtLjUyNS41MzVjMCAuMjkzLjIzNC41MzEuNTI0LjUzNWwyMy43NjUuMzE2YzIuNzI0IDAgNC4wNDYgMi41NiAzLjMxNiA1LjA4MWwtLjMwNyAxLjA2MWEuNzUuNzUgMCAwIDEtLjcxNi41NDFILjk2NGEuNzEuNzEgMCAwIDEtLjcxOC0uNjQ2IDExIDExIDAgMCAxLS4wMzQtLjg1NGMwLTUuNDc2IDQuMTY2LTkuOTczIDkuNDgyLTEwLjQ1NmE3LjIgNy4yIDAgMCAxLS4xNjctMS41NDRjMC0zLjkzNSAzLjE3LTcuMTI1IDcuMDgtNy4xMjUgMS41NTUgMCAyLjk5NC41MDYgNC4xNjIgMS4zNjJDMjIuODQ5IDQuNDk3IDI4LjcwNyAwIDM1LjYwOSAwIi8+PC9nPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDBfNzQ2XzIiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0wIDBoNjZ2MzBIMHoiLz48L2NsaXBQYXRoPjwvZGVmcz48L3N2Zz4=)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


Skip to content

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.