Skip to main content
Preview URLs expose one machine port through Nullspace ingress. The SDK returns signed HTTP and WebSocket URLs when edge ingress is active; use those returned URLs directly instead of constructing them by hand. For compatibility, the Python SDK still exposes helpers named machine.get_url(port) and machine.get_websocket_url(port). They return preview URLs. Older docs and scripts may call this surface “public URLs”; the current product name is Preview URLs.

Start a server

from nullspace import Machine

with Machine.create(template="base", timeout=300) as machine:
    machine.files.write("/workspace/index.html", "hello\n")
    server = machine.commands.run(
        "python3 -m http.server 8080 --bind 0.0.0.0",
        shell=True,
        cwd="/workspace",
        background=True,
    )
    try:
        print(f"Preview URL: {machine.get_url(8080)}")
        input("Open the URL, then press Enter to stop the server and destroy the machine...")
    finally:
        server.kill()

Host information

info = machine.get_host_info(8080)
print(info.host)
print(info.url)
print(info.websocket_url)
print(info.access_token_expires_at)
machine.get_url(port) returns info.url when a signed HTTP URL is present and otherwise falls back to the bare host with the API scheme. get_websocket_url does the same for WebSocket URLs.

WebSocket URL for exposed ports

The Python SDK exposes a dedicated get_websocket_url helper. The CLI selects the signed WebSocket URL with --websocket, and the HTTP API returns websocket_url from the same host call. The TypeScript SDK does not have a separate WebSocket-URL helper; read websocketUrl from machine.getHost(port) shown above.
ws_url = machine.get_websocket_url(8080)
print(ws_url)
Use this for WebSocket servers running inside the machine. Direct HTTP preview continuation cookies do not authorize WebSocket upgrades; use the signed websocket_url returned by the SDK or CLI. For SSH, use SSH Access instead of preview URL routing. Only older port-22 fallback deployments use SSH as an exposed WebSocket service.

Behavior

The returned URL includes a signed edge token. Treat it as a bearer credential and rotate by requesting a fresh preview URL when needed. After a browser opens the signed HTTP URL, Nullspace edge can use a scoped HTTP-only continuation cookie so normal navigation, refreshes, relative links, redirects, and assets do not need to preserve the query token. The service inside the machine must bind to 0.0.0.0, not only 127.0.0.1.

Self-hosted ingress

The single-host OSS appliance uses API-compatible ingress, not apps/edge. In localhost/no-domain mode, the appliance leaves NULLSPACE_PUBLIC_HOSTNAME unset. Caddy serves the console at /console and proxies API/WebSocket routes to the local API, but signed public preview hostnames are not available. Use machine.get_host(port) or machine.get_host_info(port) for direct local port mappings in private operator-only environments. In owned-domain mode, operators set NULLSPACE_PUBLIC_HOSTNAME to a DNS name they control and configure wildcard DNS so preview subdomains resolve to the single host. Signed preview URLs use https://{PORT}-{MACHINE_ID}.${NULLSPACE_PUBLIC_HOSTNAME}/?edge_token=... for HTTP and the matching wss:// URL for WebSocket. Caddy obtains exact-host preview certificates on demand through a loopback-only API ask endpoint, then proxies those hosts to nullspace-api while preserving the original Host and forwarded client/proto headers so the API can parse the machine and port without apps/edge. The launch gate records the difference explicitly. In --mode localhost, it asserts direct get_host/get_host_info mappings and no signed URL metadata. In --mode owned-domain, it asserts signed HTTP and WebSocket preview URL metadata and Caddy routing without apps/edge.

Embedding

Direct preview links are not iframe targets. Their browser continuation cookie is host-scoped and uses SameSite=Lax. No SameSite=None direct preview cookie mode is available for third-party iframe embedding. Customer-run preview proxies are the supported path for embedding, custom browser sessions, and custom-domain presentation.

Warnings and CORS

Nullspace does not add a preview warning or interstitial before serving direct preview traffic. Treat signed preview URLs as bearer credentials and share them only with trusted recipients. Preview CORS and browser response headers come from the machine service or the customer-run proxy in front of it. Nullspace preview edge does not inject a separate CORS policy for direct preview links.

Expiry, inventory, and revocation

Use machine.create_signed_preview_url(port, expires_in_seconds=...) or nullspace machine preview-url create <id> <port> --expires 15m when you need a durable grant with an explicit expiry. Use machine.list_preview_urls() or nullspace machine preview-url list <id> to inspect active and recent grants, and machine.revoke_preview_url(grant_id) or nullspace machine preview-url revoke <id> <grant-id> to revoke a grant before it expires. Grant inventory is token-redacted. It includes status, expiry, first and last use, validation use count, HTTP request count, WebSocket connection count, byte-in and byte-out counters, last error code/time, and disabled time when an operator has temporarily disabled a grant. Operator audit records use the same safe identifiers and never store raw preview tokens. Direct preview URLs use query-token bootstrap auth for HTTP and WebSocket URLs. Custom preview proxy targets use header-token auth instead, so customer-run proxies can keep Nullspace bearer credentials out of browser-visible URLs. Agent service deployments reuse the same ingress path. nullspace agent url and the SDK service URL helper return a dynamic URL only after the service is ready and permissions.public_url is enabled. Edge environments return either a host-style or path-style URL depending on operator configuration; both forms can include a short-lived edge_token query parameter. Do not store these URLs in deployment config, logs, or source files. If service permissions disable public traffic, ingress may also require the x-nullspace-traffic-access-token header documented in Access control. The Console Preview tab exposes the same workflow for a machine: create a direct preview link, open it in a new browser tab, copy raw secret values only after confirmation, create a custom preview proxy target, inspect grant inventory, revoke grants, and run readiness diagnostics.

Port policy

Preview URLs support normal application ports in the 1-65535 range, including privileged HTTP ports such as 80 and 443. Generic preview URLs do not expose platform-owned guest ports:
PortUse
22SSH access; use SSH Access instead of preview URLs.
5900-5999Desktop/VNC traffic; use the managed desktop viewer.
Host service ports such as the API, host-agent, and edge listener ports are operator configuration, not guest preview policy. Common app ports such as 3000 and 8080 remain valid machine preview ports.

Readiness

Creating or resolving a preview URL confirms that routing exists; it does not wait for the server process inside the machine to listen. Use the SDK or CLI wait helpers when startup is asynchronous. The TypeScript SDK does not expose a preview readiness helper, so use the Python SDK, CLI, or HTTP API:
readiness = machine.wait_for_preview(8080, timeout_secs=30)
if not readiness.ready:
    print(readiness.error)
    print(readiness.suggested_action)
If readiness fails, confirm the service is running on the requested port and binds to 0.0.0.0 inside the machine rather than only 127.0.0.1. Use Custom Preview Proxy when you need your own domain, app session checks, or proxy middleware in front of a machine preview. That flow returns a marker-only upstream URL and a header token for your proxy to send to Nullspace edge, so browser-visible URLs do not contain Nullspace bearer credentials. A managed custom preview domain workflow is not part of the current launch. If the machine was created with on_timeout="pause", auto_resume=True, HTTP and WebSocket requests to signed preview URLs can wake a paused machine. When auto-resume is disabled or the wake cannot finish in time, callers receive 503 Service Unavailable with Retry-After: 5 and JSON fields for code, retryable, and suggested_action.

Troubleshooting

Code or symptomWhat to do
unsupported_preview_portUse an application port in 1-65535 except 22 and 5900-5999.
preview_service_not_readyConfirm the server is running on that port and binding to 0.0.0.0; use machine.wait_for_preview(port) while it starts.
edge_token_missingUse the full signed URL returned by machine.get_url(port) or request a fresh signed preview URL.
edge_token_expired, preview_grant_revokedRequest a fresh preview URL before retrying the direct browser request.
preview_grant_disabled, preview_proxy_token_disabled, preview_machine_disabledPreview access was disabled by an operator control. Use another route or ask an operator to re-enable preview traffic before retrying.
preview_proxy_token_missingForward the x-nullspace-preview-proxy-token header returned by create_preview_proxy_target.
preview_proxy_token_expired, preview_proxy_token_revokedCreate a fresh preview proxy target and update the proxy header token.
rate_limit_exceededWait for the Retry-After window before retrying. Repeated 429s usually mean too many requests from the same preview grant, port, transport, and client address.
edge_runtime_host_unreachableCheck readiness with machine.wait_for_preview(port), nullspace machine preview-url create <id> <port> --wait, or nullspace machine url <id> <port> --wait, then retry.
machine_paused_auto_resume_disabledResume the machine manually or recreate it with on_timeout="pause", auto_resume=True.
Localhost self-host returns no signed URL metadataExpected when NULLSPACE_PUBLIC_HOSTNAME is unset. Use direct get_host_info() mappings or configure owned-domain mode.
Owned-domain self-host returns direct mappingsConfirm NULLSPACE_PUBLIC_HOSTNAME, NULLSPACE_EDGE_PUBLIC_BASE_URL, NULLSPACE_EDGE_ACCESS_TOKEN_SIGNING_KEY, and NULLSPACE_PUBLIC_INGRESS_MODE=api_compat, then rerun nullspace-host launch-gate --mode owned-domain.
Auto-resume timeout or control-plane unreachableRetry after Retry-After; if it repeats, check runtime capacity and machine startup logs.

Create-time network controls

Machine create accepts a network dictionary for deployment-supported network policy controls and an internet_access flag for outbound internet access:
machine = Machine.create(
    template="base",
    internet_access=True,
    network={
        "allow_public_traffic": True,
        "mask_request_host": "localhost:${PORT}",
        "deny_out": ["0.0.0.0/0"],
        "allow_out": ["10.0.0.0/8"],
    },
)
Use Access control for private preview URLs, traffic tokens, outbound network policy, and Host header masking. Use Token model for the difference between API keys, edge preview tokens, preview continuation cookies, proxy tokens, and private traffic tokens.