Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/content/docs/sandbox/api/ports.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExposePortResponse>` with `port`, `url` (preview URL), `name`
Expand All @@ -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');
Expand Down
41 changes: 30 additions & 11 deletions src/content/docs/sandbox/concepts/preview-urls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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!)
```

Expand All @@ -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!)
```

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
71 changes: 66 additions & 5 deletions src/content/docs/sandbox/guides/expose-services.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@
await sandbox.startProcess('python -m http.server 8000');

// 2. Wait for service to start
await new Promise(resolve => setTimeout(resolve, 2000));

Check warning on line 51 in src/content/docs/sandbox/guides/expose-services.mdx

View workflow job for this annotation

GitHub Actions / Semgrep

semgrep.style-guide-potential-date-year

Potential year found. Documentation should strive to represent universal truth, not something time-bound. (add [skip style guide checks] to commit message to skip)

// 3. Expose the port
const exposed = await sandbox.exposePort(8000, { hostname });

// 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 });
Expand All @@ -73,7 +73,7 @@
:::

:::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:

Expand All @@ -88,6 +88,42 @@
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:

<TypeScriptExample>
```
// 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
```
</TypeScriptExample>

**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:
Expand All @@ -99,12 +135,12 @@

// Start and expose API server
await sandbox.startProcess('node api.js', { env: { PORT: '8080' } });
await new Promise(resolve => setTimeout(resolve, 2000));

Check warning on line 138 in src/content/docs/sandbox/guides/expose-services.mdx

View workflow job for this annotation

GitHub Actions / Semgrep

semgrep.style-guide-potential-date-year

Potential year found. Documentation should strive to represent universal truth, not something time-bound. (add [skip style guide checks] to commit message to skip)
const api = await sandbox.exposePort(8080, { hostname, name: 'api' });

// Start and expose frontend
await sandbox.startProcess('npm run dev', { env: { PORT: '5173' } });
await new Promise(resolve => setTimeout(resolve, 2000));

Check warning on line 143 in src/content/docs/sandbox/guides/expose-services.mdx

View workflow job for this annotation

GitHub Actions / Semgrep

semgrep.style-guide-potential-date-year

Potential year found. Documentation should strive to represent universal truth, not something time-bound. (add [skip style guide checks] to commit message to skip)
const frontend = await sandbox.exposePort(5173, { hostname, name: 'frontend' });

console.log('Services:');
Expand All @@ -126,7 +162,7 @@
await sandbox.startProcess('npm run dev', { env: { PORT: '8080' } });

// Wait 2-3 seconds
await new Promise(resolve => setTimeout(resolve, 2000));

Check warning on line 165 in src/content/docs/sandbox/guides/expose-services.mdx

View workflow job for this annotation

GitHub Actions / Semgrep

semgrep.style-guide-potential-date-year

Potential year found. Documentation should strive to represent universal truth, not something time-bound. (add [skip style guide checks] to commit message to skip)

// Now expose
await sandbox.exposePort(8080, { hostname });
Expand Down Expand Up @@ -169,7 +205,7 @@
await sandbox.startProcess('node api/server.js', {
env: { PORT: '8080' }
});
await new Promise(resolve => setTimeout(resolve, 2000));

Check warning on line 208 in src/content/docs/sandbox/guides/expose-services.mdx

View workflow job for this annotation

GitHub Actions / Semgrep

semgrep.style-guide-potential-date-year

Potential year found. Documentation should strive to represent universal truth, not something time-bound. (add [skip style guide checks] to commit message to skip)

// Start frontend
await sandbox.startProcess('npm run dev', {
Expand Down Expand Up @@ -330,13 +366,38 @@

## 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

<TypeScriptExample>
```
// 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
```
</TypeScriptExample>

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
Expand Down
Loading