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
108 changes: 99 additions & 9 deletions src/content/docs/sandbox/api/ports.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Expose services running in your sandbox via public preview URLs. See [Preview UR

### `exposePort()`

Expose a port and get a preview URL.
Expose a port and get a preview URL for accessing services running in the sandbox.

```ts
const response = await sandbox.exposePort(port: number, options: ExposePortOptions): Promise<ExposePortResponse>
Expand All @@ -27,35 +27,125 @@ 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.
- `hostname` - Your Worker's domain name (e.g., `'example.com'`). Required to construct preview URLs with wildcard subdomains like `https://8080-sandbox-abc123token.example.com`. Cannot be a `.workers.dev` domain as it doesn't support wildcard DNS patterns.
- `name` - Friendly name for the port (optional)
- `token` - Custom token for the preview URL (optional). Must be 1-16 characters containing only lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_). If not provided, a random 16-character token is generated automatically.

**Returns**: `Promise<ExposePortResponse>` with `port`, `url` (preview URL), `name`

<TypeScriptExample>
```
```ts
// Extract hostname from request
const { hostname } = new URL(request.url);

// Basic usage with auto-generated token
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
console.log('Available at:', exposed.url);
// https://8000-sandbox-id-abc123random4567.yourdomain.com

// Multiple services with names
// With custom token for stable URLs across deployments
await sandbox.startProcess('node api.js');
const api = await sandbox.exposePort(3000, { hostname, name: 'api' });
const api = await sandbox.exposePort(3000, {
hostname,
name: 'api',
token: 'prod-api-v1' // URL stays same across restarts
});

console.log('Stable API URL:', api.url);
// https://3000-sandbox-id-prod-api-v1.yourdomain.com

// Multiple services with custom tokens
await sandbox.startProcess('npm run dev');
const frontend = await sandbox.exposePort(5173, { hostname, name: 'frontend' });
const frontend = await sandbox.exposePort(5173, {
hostname,
name: 'frontend',
token: 'dev-ui'
});
```
</TypeScriptExample>

:::note[Local development]
When using `wrangler dev`, you must add `EXPOSE` directives to your Dockerfile for each port. See [Expose Services guide](/sandbox/guides/expose-services/#local-development) for details.
:::

## Custom Tokens for Stable URLs

Custom tokens enable consistent preview URLs across container restarts and deployments. This is useful for:

- **Production environments** - Share stable URLs with users or teams
- **Development workflows** - Maintain bookmarks and integrations
- **CI/CD pipelines** - Reference consistent URLs in tests or deployment scripts

**Token Requirements:**
- 1-16 characters in length
- Only lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_)
- Must be unique per sandbox (cannot reuse tokens across different ports)

<TypeScriptExample>
```ts
// Production API with stable URL
const { url } = await sandbox.exposePort(8080, {
hostname: 'api.example.com',
token: 'v1-stable' // Always the same URL
});

// Error: Token collision prevention
await sandbox.exposePort(8081, { hostname, token: 'v1-stable' });
// Throws: Token 'v1-stable' is already in use by port 8080

// Success: Re-exposing same port with same token (idempotent)
await sandbox.exposePort(8080, { hostname, token: 'v1-stable' });
// Works - same port, same token
```
</TypeScriptExample>

### `validatePortToken()`

Validate if a token is authorized to access a specific exposed port. Useful for custom authentication or routing logic.

```ts
const isValid = await sandbox.validatePortToken(port: number, token: string): Promise<boolean>
```

**Parameters**:

- `port` - Port number to check
- `token` - Token to validate

**Returns**: `Promise<boolean>` - `true` if token is valid for the port, `false` otherwise

<TypeScriptExample>
```ts
// Custom validation in your Worker
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);

// Extract token from custom header or query param
const customToken = request.headers.get('x-access-token');

if (customToken) {
const sandbox = getSandbox(env.Sandbox, 'my-sandbox');
const isValid = await sandbox.validatePortToken(8080, customToken);

if (!isValid) {
return new Response('Invalid token', { status: 403 });
}
}

// Handle preview URL routing
const proxyResponse = await proxyToSandbox(request, env);
if (proxyResponse) return proxyResponse;

// Your application routes
return new Response('Not found', { status: 404 });
}
};
```
</TypeScriptExample>

### `unexposePort()`

Remove an exposed port and close its preview URL.
Expand Down Expand Up @@ -137,4 +227,4 @@ export default {
## Related resources

- [Preview URLs concept](/sandbox/concepts/preview-urls/) - How preview URLs work
- [Commands API](/sandbox/api/commands/) - Start background processes
- [Commands API](/sandbox/api/commands/) - Start background processes
66 changes: 51 additions & 15 deletions src/content/docs/sandbox/concepts/preview-urls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,56 @@ const { hostname } = new URL(request.url);
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
console.log(exposed.url);
// Production: https://8000-sandbox-id-abc123random4567.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`
- With auto-generated token: `https://8080-abc123-random16chars12.yourdomain.com`
- With custom token: `https://8080-abc123-my-api-v1.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

### Auto-generated tokens (default)

When no custom token is specified, a random 16-character token is generated:

```typescript
const exposed = await sandbox.exposePort(8000, { hostname });
// https://8000-sandbox-id-abc123random4567.yourdomain.com
```

URLs with auto-generated tokens change when you unexpose and re-expose a port.

### Custom tokens for stable URLs

For production deployments or shared URLs, specify a custom token to maintain consistency across container restarts:

```typescript
const stable = await sandbox.exposePort(8000, {
hostname,
token: 'api-v1'
});
// https://8000-sandbox-id-api-v1.yourdomain.com
// Same URL every time ✓
```

**Token requirements:**
- 1-16 characters long
- Lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_) only
- Must be unique within each sandbox

**Use cases for custom tokens:**
- Production APIs with stable endpoints
- Sharing demo URLs with external users
- Documentation with consistent examples
- Integration testing with predictable URLs

## ID Case Sensitivity

Expand All @@ -45,7 +80,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: 8080-myproject-123-token123.yourdomain.com
// Routes to: "myproject-123" (different DO - doesn't exist!)
```

Expand All @@ -56,7 +91,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: 8080-myproject-123-token123.yourdomain.com
// Routes to: "myproject-123" (same DO - works!)
```

Expand Down Expand Up @@ -101,9 +136,9 @@ await sandbox.startProcess("node admin.js"); // Port 3001
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
// Each gets its own URL with unique tokens:
// https://3000-abc123-random16chars01.yourdomain.com
// https://3001-abc123-random16chars02.yourdomain.com
```

## What Works
Expand Down Expand Up @@ -131,10 +166,10 @@ const { hostname } = new URL(request.url);

// Start a WebSocket server
await sandbox.startProcess("bun run ws-server.ts 8080");
const { exposedAt } = await sandbox.exposePort(8080, { hostname });
const { url } = 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 All @@ -155,9 +190,10 @@ Preview URLs are publicly accessible by default, but require a valid access toke

**Built-in security**:

- **Token-based access** - Each exposed port gets a unique token in the URL (for example, `https://8080-sandbox-abc123token.yourdomain.com`)
- **Token-based access** - Each exposed port gets a unique token in the URL (for example, `https://8080-sandbox-abc123token456.yourdomain.com`)
- **HTTPS in production** - All traffic is encrypted with automatic TLS
- **Unpredictable URLs** - Tokens are randomly generated and difficult to guess
- **Unpredictable URLs** - Auto-generated tokens are randomly generated and difficult to guess
- **Token collision prevention** - Custom tokens are validated to ensure uniqueness within each sandbox

**Add application-level authentication**:

Expand Down
61 changes: 54 additions & 7 deletions src/content/docs/sandbox/guides/expose-services.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
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 });
Expand Down Expand Up @@ -88,6 +88,45 @@
See [Sandbox options](/sandbox/configuration/sandbox-options/#normalizeid) for details.
:::

## Stable URLs with custom tokens

For production deployments or when sharing URLs with users, use custom tokens to maintain consistent preview URLs across container restarts:

<TypeScriptExample>
```
// Extract hostname from request
const { hostname } = new URL(request.url);

// Without custom token - URL changes on restart
const exposed = await sandbox.exposePort(8080, { hostname });
// https://8080-sandbox-id-random16chars12.yourdomain.com

// With custom token - URL stays the same across restarts
const stable = await sandbox.exposePort(8080, {
hostname,
token: 'api-v1'
});
// https://8080-sandbox-id-api-v1.yourdomain.com
// Same URL after container restart ✓

return Response.json({
'Temporary URL (changes on restart)': exposed.url,
'Stable URL (consistent)': stable.url
});
```
</TypeScriptExample>

**Token requirements:**
- 1-16 characters long
- Lowercase letters (a-z), numbers (0-9), hyphens (-), and underscores (_) only
- Must be unique within each sandbox

**Use cases:**
- Production APIs with stable endpoints
- Sharing demo URLs with external users
- Integration testing with predictable URLs
- Documentation with consistent examples

## Name your exposed ports

When exposing multiple ports, use names to stay organized:
Expand All @@ -97,15 +136,23 @@
// Extract hostname from request
const { hostname } = new URL(request.url);

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

Check warning on line 141 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' });
const api = await sandbox.exposePort(8080, {
hostname,
name: 'api',
token: 'api-prod'
});

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

Check warning on line 150 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' });
const frontend = await sandbox.exposePort(5173, {
hostname,
name: 'frontend',
token: 'web-app'
});

console.log('Services:');
console.log('- API:', api.exposedAt);
Expand All @@ -126,7 +173,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 176 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 +216,7 @@
await sandbox.startProcess('node api/server.js', {
env: { PORT: '8080' }
});
await new Promise(resolve => setTimeout(resolve, 2000));

Check warning on line 219 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,10 +377,10 @@

## 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`
- Auto-generated token: `https://8080-abc123-random16chars12.yourdomain.com`
- Custom token: `https://8080-abc123-my-api-v1.yourdomain.com`

**Local development**: `http://localhost:8787/...`

Expand Down
Loading