diff --git a/src/content/docs/sandbox/api/ports.mdx b/src/content/docs/sandbox/api/ports.mdx index 53e789c109ac55..18267aa51e50c1 100644 --- a/src/content/docs/sandbox/api/ports.mdx +++ b/src/content/docs/sandbox/api/ports.mdx @@ -28,6 +28,7 @@ const response = await sandbox.exposePort(port: number, options: ExposePortOptio - `port` - Port number to expose (1024-65535) - `options`: - `hostname` - Your Worker's domain name (e.g., `'example.com'`). Required to construct preview URLs with wildcard subdomains like `https://8080-sandbox-abc123.example.com`. Cannot be a `.workers.dev` domain as it doesn't support wildcard DNS patterns. + - `token` - Custom token for stable preview URLs (optional). Must be 1-16 characters containing only lowercase letters (a-z), numbers (0-9), and underscores (_). When provided, enables consistent URLs across deployments and restarts. - `name` - Friendly name for the port (optional) **Returns**: `Promise` with `port`, `url` (preview URL), `name` @@ -41,7 +42,15 @@ await sandbox.startProcess('python -m http.server 8000'); const exposed = await sandbox.exposePort(8000, { hostname }); console.log('Available at:', exposed.exposedAt); -// https://8000-abc123.yourdomain.com +// https://8000-abc123-randomtoken123.yourdomain.com + +// Custom token for stable URLs across deployments +const stable = await sandbox.exposePort(8080, { + hostname, + token: 'my_app_v1' +}); +console.log('Stable URL:', stable.exposedAt); +// https://8080-abc123-my_app_v1.yourdomain.com // Multiple services with names await sandbox.startProcess('node api.js'); diff --git a/src/content/docs/sandbox/concepts/preview-urls.mdx b/src/content/docs/sandbox/concepts/preview-urls.mdx index 1d98a3b8e41d48..7149c6027cb8ed 100644 --- a/src/content/docs/sandbox/concepts/preview-urls.mdx +++ b/src/content/docs/sandbox/concepts/preview-urls.mdx @@ -19,24 +19,43 @@ await sandbox.startProcess("python -m http.server 8000"); const exposed = await sandbox.exposePort(8000, { hostname }); console.log(exposed.exposedAt); -// Production: https://8000-abc123.yourdomain.com +// Production: https://8000-abc123-randomtoken123.yourdomain.com // Local dev: http://localhost:8787/... ``` ## URL Format -**Production**: `https://{port}-{sandbox-id}.yourdomain.com` +**Production**: `https://{port}-{sandbox-id}-{token}.yourdomain.com` -- Port 8080: `https://8080-abc123.yourdomain.com` -- Port 3000: `https://3000-abc123.yourdomain.com` +- Port 8080: `https://8080-abc123-randomtoken123.yourdomain.com` +- Port 3000: `https://3000-abc123-anothertokenabc.yourdomain.com` **Local development**: `http://localhost:8787/...` -Preview URLs remain stable while a port is exposed and can be shared during that time. However, if you unexpose and re-expose a port, a new random token is generated and the URL changes. For persistent URLs, keep ports exposed for the duration you need them accessible. +### Token Types + +**Random tokens** (default): If you do not provide a custom token, each exposed port gets a unique, randomly generated 16-character token. These URLs remain stable while a port is exposed, but change if you unexpose and re-expose. + +**Custom tokens**: You can provide a custom token for stable URLs that persist across deployments and restarts: + +```typescript +// Custom token - URL stays consistent +const stable = await sandbox.exposePort(8080, { + hostname, + token: 'my_app_v1' +}); +// URL: https://8080-abc123-my_app_v1.yourdomain.com + +// Without token - generates random token +const random = await sandbox.exposePort(8080, { hostname }); +// URL: https://8080-abc123-abc123random4567.yourdomain.com +``` + +Custom tokens must be 1-16 characters containing only lowercase letters (a-z), numbers (0-9), and underscores (_). ## ID Case Sensitivity -Preview URLs extract the sandbox ID from the hostname to route requests. Since hostnames are case-insensitive (per RFC 3986), they're always lowercased: `8080-MyProject-123.yourdomain.com` becomes `8080-myproject-123.yourdomain.com`. +Preview URLs extract the sandbox ID from the hostname to route requests. Since hostnames are case-insensitive (per RFC 3986), they're always lowercased: `8080-MyProject-123-token.yourdomain.com` becomes `8080-myproject-123-token.yourdomain.com`. **The problem**: If you create a sandbox with `"MyProject-123"`, it exists as a Durable Object with that exact ID. But the preview URL routes to `"myproject-123"` (lowercased from the hostname). These are different Durable Objects, so your sandbox is unreachable via preview URL. @@ -45,7 +64,7 @@ Preview URLs extract the sandbox ID from the hostname to route requests. Since h const sandbox = getSandbox(env.Sandbox, 'MyProject-123'); // Durable Object ID: "MyProject-123" await sandbox.exposePort(8080, { hostname }); -// Preview URL: 8000-myproject-123.yourdomain.com +// Preview URL: 8000-myproject-123-token123.yourdomain.com // Routes to: "myproject-123" (different DO - doesn't exist!) ``` @@ -56,7 +75,7 @@ const sandbox = getSandbox(env.Sandbox, 'MyProject-123', { normalizeId: true }); // Durable Object ID: "myproject-123" (lowercased) -// Preview URL: 8000-myproject-123.yourdomain.com +// Preview URL: 8000-myproject-123-token123.yourdomain.com // Routes to: "myproject-123" (same DO - works!) ``` @@ -102,8 +121,8 @@ const api = await sandbox.exposePort(3000, { hostname, name: "api" }); const admin = await sandbox.exposePort(3001, { hostname, name: "admin" }); // Each gets its own URL: -// https://3000-abc123.yourdomain.com -// https://3001-abc123.yourdomain.com +// https://3000-abc123-token123.yourdomain.com +// https://3001-abc123-token456.yourdomain.com ``` ## What Works @@ -134,7 +153,7 @@ await sandbox.startProcess("bun run ws-server.ts 8080"); const { exposedAt } = await sandbox.exposePort(8080, { hostname }); // Clients connect using WebSocket protocol -// Browser: new WebSocket('wss://8080-abc123.yourdomain.com') +// Browser: new WebSocket('wss://8080-abc123-token123.yourdomain.com') // Your Worker routes automatically export default { diff --git a/src/content/docs/sandbox/guides/expose-services.mdx b/src/content/docs/sandbox/guides/expose-services.mdx index ee82fb05d96dbe..80ff4be18e7cb1 100644 --- a/src/content/docs/sandbox/guides/expose-services.mdx +++ b/src/content/docs/sandbox/guides/expose-services.mdx @@ -55,7 +55,7 @@ export default { // 4. Preview URL is now available (public by default) console.log('Server accessible at:', exposed.exposedAt); - // Production: https://8000-abc123.yourdomain.com + // Production: https://8000-abc123-randomtoken123.yourdomain.com // Local dev: http://localhost:8787/... return Response.json({ url: exposed.exposedAt }); @@ -73,7 +73,7 @@ When using `wrangler dev`, you must add `EXPOSE` directives to your Dockerfile f ::: :::caution[Uppercase sandbox IDs don't work with preview URLs] -Preview URLs extract the sandbox ID from the hostname, which is always lowercase (e.g., `8000-myproject-123.yourdomain.com`). If you created your sandbox with an uppercase ID like `"MyProject-123"`, the URL routes to `"myproject-123"` (a different Durable Object), making your sandbox unreachable. +Preview URLs extract the sandbox ID from the hostname, which is always lowercase (e.g., `8000-myproject-123-token.yourdomain.com`). If you created your sandbox with an uppercase ID like `"MyProject-123"`, the URL routes to `"myproject-123"` (a different Durable Object), making your sandbox unreachable. To fix this, use `normalizeId: true` when creating sandboxes for port exposure: @@ -88,6 +88,42 @@ This lowercases the ID during creation so it matches preview URL routing. Withou See [Sandbox options](/sandbox/configuration/sandbox-options/#normalizeid) for details. ::: +## Stable URLs with custom tokens + +By default, each port gets a random token that changes when you re-expose the port. For stable URLs that persist across deployments, use custom tokens: + + +``` +// Extract hostname from request +const { hostname } = new URL(request.url); + +// Custom token - URL stays the same across restarts/deployments +const api = await sandbox.exposePort(8080, { + hostname, + token: 'production_api', + name: 'api' +}); +console.log('Stable API URL:', api.exposedAt); +// Always: https://8080-abc123-production_api.yourdomain.com + +// Without token - generates random token each time +const dev = await sandbox.exposePort(3001, { hostname, name: 'dev' }); +console.log('Random URL:', dev.exposedAt); +// Changes: https://3001-abc123-abc123random4567.yourdomain.com +``` + + +**Custom token requirements:** +- 1-16 characters +- Only lowercase letters (a-z), numbers (0-9), and underscores (_) +- Must be unique per sandbox (cannot reuse tokens for different ports) + +**Use cases for custom tokens:** +- Production services that need stable URLs +- Sharing URLs with team members +- Webhook endpoints that cannot change +- Documentation links + ## Name your exposed ports When exposing multiple ports, use names to stay organized: @@ -330,13 +366,38 @@ See [Sandbox options - normalizeId](/sandbox/configuration/sandbox-options/#norm ## Preview URL Format -**Production**: `https://{port}-{sandbox-id}.yourdomain.com` +**Production**: `https://{port}-{sandbox-id}-{token}.yourdomain.com` -- Port 8080: `https://8080-abc123.yourdomain.com` -- Port 5173: `https://5173-abc123.yourdomain.com` +- Port 8080: `https://8080-abc123-randomtoken123.yourdomain.com` +- Port 5173: `https://5173-abc123-anothertokenabc.yourdomain.com` **Local development**: `http://localhost:8787/...` +### Token Types + +Each exposed port includes a unique token in the URL for security: + +- **Random tokens** (default): Automatically generated 16-character tokens +- **Custom tokens**: Use your own tokens for stable URLs across deployments + + +``` +// Custom token for stable URLs +const api = await sandbox.exposePort(8080, { + hostname, + token: 'my_api_v1', + name: 'api' +}); +// URL: https://8080-abc123-my_api_v1.yourdomain.com + +// Random token (default) +const frontend = await sandbox.exposePort(5173, { hostname, name: 'frontend' }); +// URL: https://5173-abc123-abc123random4567.yourdomain.com +``` + + +Custom tokens must be 1-16 characters containing only lowercase letters (a-z), numbers (0-9), and underscores (_). + **Note**: Port 3000 is reserved for the internal Bun server and cannot be exposed. ## Related resources