Reach a port in a workspace

A private HTTPS URL for any port of a workspace. A request wakes the workspace, so previews, webhooks and OAuth callbacks always reach your app.

What a port URL does

Any TCP port of a workspace can get its own HTTPS URL:

Text
https://3000-k7q2m4x9a3b5c6d7.shardflux.app
  • It wakes the workspace inside the request. A running workspace answers at once. A parked one is woken by the request itself, and a suspended one is resumed while the request waits (up to 30 seconds), with its memory and processes as they were. Your dev server, API or app needs nothing special: it keeps running across every suspend.
  • Every port is private. A request carries a port token, the cookie of a signed link, or arrives on the port's callback URL. Anything else is refused before it reaches the workspace, and a refused request never wakes it.
  • HTTP/1.1, streaming and WebSockets pass through as they are. An open request or WebSocket keeps the workspace awake, and the idle timeout runs from the last request.
Client Version Expose Token Link Callback URL
TypeScript SDK @shardflux/sdk (0.14.0+) workspace.ports.expose(3000) ports.token(3000) ports.link(3000) ports.createCallbackUrl(3000)
Python SDK shardflux (0.10.0+) ws.ports.expose(3000) ws.ports.token(3000) ws.ports.link(3000) ws.ports.create_callback_url(3000)
CLI @shardflux/cli (0.9.0+) shard ws port expose <key> 3000 shard ws port token shard ws port link shard ws port callback
MCP server @shardflux/mcp (0.8.0+) workspace_expose_port workspace_port_link
HTTP /v1 PUT /workspaces/{id}/ports/3000 POST .../ports/3000/tokens POST .../ports/3000/links POST .../ports/3000/callback

Expose a port

Start your server so it listens on all interfaces (0.0.0.0), then expose its port:

TypeScript
await workspace.cell().exec.start({ argv: ['python3', '-m', 'http.server', '3000', '--bind', '0.0.0.0'] });
const { url } = await workspace.ports.expose(3000);
console.log(url); // https://3000-k7q2m4x9a3b5c6d7.shardflux.app
Python
port = ws.ports.expose(3000)
print(port.url)  # https://3000-k7q2m4x9a3b5c6d7.shardflux.app
Shell
shard ws port expose customer-42/main 3000
shard ws port ls customer-42/main
  • Exposing is idempotent: exposing an exposed port returns it unchanged, and its tokens and links stay valid.
  • The URL is stable for the workspace's whole life, across suspends, resumes and resets. A fork gets its own URLs and starts with no ports exposed.
  • A server bound only to 127.0.0.1 is not reachable from outside the workspace. Most dev servers take --host 0.0.0.0. Vite also checks the Host header: add server: { host: true, allowedHosts: ['.shardflux.app'] }.
  • Your app sees the public Host, and X-Forwarded-For, X-Forwarded-Proto: https and X-Forwarded-Host, so it builds correct absolute URLs and secure cookies.

Call it from code: port tokens

A port token authorizes requests to one port of one workspace. Send it as a bearer token:

TypeScript
const { token } = await workspace.ports.token(3000, { ttlSeconds: 3600 });
const res = await fetch(`${url}/api/health`, { headers: { Authorization: `Bearer ${token}` } });
Python
token = ws.ports.token(3000)
httpx.get(f"{port.url}/api/health", headers={"Authorization": f"Bearer {token.token}"})
Shell
curl -H "Authorization: Bearer $(shard ws port token customer-42/main 3000)" \
  https://3000-k7q2m4x9a3b5c6d7.shardflux.app/api/health
  • Tokens start with sfp_ and last 1 hour by default (ttlSeconds 60 to 86,400).
  • When your app uses the Authorization header itself, send the token as X-Shardflux-Token: sfp_... instead: your Authorization header then reaches the app untouched. The platform's own credential never reaches your app.
  • Closing the port revokes every token, link and callback URL of it, also when you expose it again.

A signed link opens the port in a browser, for you or for your own users, until it expires:

TypeScript
const { url: link } = await workspace.ports.link(3000, { path: '/dashboard', ttlSeconds: 86400 });
Shell
shard ws port link customer-42/main 3000 --path /dashboard --ttl 86400

The link sets a cookie for that port's host only (Secure, HttpOnly, SameSite=Lax) and redirects to path. Every page, script, image and WebSocket of the app then works in that browser for as long as the link is valid (1 day by default, up to 7 days). Anyone with the link can open the port until then, so share it like a password.

Webhooks and OAuth callbacks: the callback URL

Providers that call you back (GitHub and Stripe webhooks, Slack events, OAuth redirects) cannot send a token. Give them the port's callback URL instead. It carries an unguessable secret in its path:

TypeScript
const { url: callback } = await workspace.ports.createCallbackUrl(3000);
// https://3000-k7q2m4x9a3b5c6d7.shardflux.app/__shardflux/callback/sfcb_.../
// Register `${callback}webhooks/github` with GitHub.
Shell
shard ws port callback customer-42/main 3000
shard ws port callback customer-42/main 3000 --revoke
  • A request to <callback URL><path> reaches /<path> in your app (the query string is kept), and wakes the workspace like any other request. Your app verifies the provider's signature as usual.
  • The URL is shown once. Creating it again replaces it (the previous URL stops working at once); --revoke / revokeCallbackUrl() removes it.

What wakes the workspace and what keeps it awake

The workspace is A request
Running Is answered at once
Parked Wakes it inside the request, exactly as a tool call does
Suspended Resumes it, with memory and processes as they were, and is answered as soon as it runs
Starting, resuming or suspending Waits for that to finish, then as above
  • A request in progress or an open WebSocket keeps the workspace awake: it is not parked and not suspended while connected. The idle timeout counts from the end of the last request.
  • A request waits at most 30 seconds for the workspace to run and the port to accept. A server that is still starting after a resume is waited for within that time.

Billing

Bytes your app sends back through a port URL count as outbound transfer. Requests into the workspace are inbound traffic and are free. A workspace woken by a request is billed like one woken by a tool call.

Limits and errors

Ports per workspace, token and link lifetimes, request rates and timeouts are listed in Limits. Every refusal by the platform carries an x-shardflux-reason header (a response without it came from your app); the reasons are in Errors.

View this page as Markdown