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:
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:
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.appport = ws.ports.expose(3000)
print(port.url) # https://3000-k7q2m4x9a3b5c6d7.shardflux.appshard 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.1is not reachable from outside the workspace. Most dev servers take--host 0.0.0.0. Vite also checks theHostheader: addserver: { host: true, allowedHosts: ['.shardflux.app'] }. - Your app sees the public
Host, andX-Forwarded-For,X-Forwarded-Proto: httpsandX-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:
const { token } = await workspace.ports.token(3000, { ttlSeconds: 3600 });
const res = await fetch(`${url}/api/health`, { headers: { Authorization: `Bearer ${token}` } });token = ws.ports.token(3000)
httpx.get(f"{port.url}/api/health", headers={"Authorization": f"Bearer {token.token}"})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 (ttlSeconds60 to 86,400). - When your app uses the
Authorizationheader itself, send the token asX-Shardflux-Token: sfp_...instead: yourAuthorizationheader 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.
Open it in a browser: signed links
A signed link opens the port in a browser, for you or for your own users, until it expires:
const { url: link } = await workspace.ports.link(3000, { path: '/dashboard', ttlSeconds: 86400 });shard ws port link customer-42/main 3000 --path /dashboard --ttl 86400The 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:
const { url: callback } = await workspace.ports.createCallbackUrl(3000);
// https://3000-k7q2m4x9a3b5c6d7.shardflux.app/__shardflux/callback/sfcb_.../
// Register `${callback}webhooks/github` with GitHub.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.