Skip to main content
Preview URLs let you open an application running inside a workspace from your browser. When a server starts on a port, Aether’s gateway proxies public traffic to that port inside the workspace VM.

URL Format

A preview URL is built from three parts in the subdomain:
  • port — the port the server listens on inside the workspace (1–65535)
  • workspaceIdPrefix — the first 8 hex characters of the workspace ID
  • token — the workspace preview token, exactly 32 [a-z0-9] characters
For example, a Vite dev server on port 5173 in a workspace whose ID starts with abc12345:
All three segments are required. There is no token-less form — a URL without a valid 32-character token is rejected. The prefix is the workspace ID, not the project ID.

Getting a Preview URL

The web app shows preview URLs automatically when it detects an open port. Each port in the workspace’s Preview popover has a Copy link button (and, on devices with a share sheet, a Share button), and the in-app preview panel’s address bar has the same copy control. To build a URL from the CLI:
This connects to the workspace, reads its preview token, and prints the full URL. Preview links are made to be shared: they work in any browser with no Aether login, so you can paste one into a pull request, drop it in Slack, or open it on your phone. Anyone with the link can open the preview while the workspace is running — treat it like a password. The token is stable for the workspace’s lifetime, so a shared link keeps working across reconnects and restarts.

Authenticated previews

A repository can declare a per-port auth_command that mints a development session inside the workspace. There are two ways to apply it. Set auto: true beside auth_command. On the first visit, the ordinary preview URL—including the plain link in a pull-request preview comment—starts the hook and applies its result, so a hook that navigates or reloads lands the visitor authenticated without a decorated URL or an Aether login. The request is asynchronous and application initialization can continue before its result arrives; startup-only clients that return local_storage must also return reload: true. The preview runtime records its attempt in sessionStorage before it calls the hook. It therefore tries silently at most once per tab session at that preview origin: if the hook, network, response, or session application fails, the visitor stays on the plain preview and later reloads in the same tab do not retry. During a mixed-fleet rollout, older workspaces simply do not bootstrap; their plain previews continue to work unchanged.

On demand with --authed

To run the hook explicitly from the CLI:
The CLI connects to the workspace so the hook receives every currently discovered sibling port, calls the authenticated Aether API, and prints a bootstrap URL when the hook returns the url shape. Open that URL to land in the application with the session the repository minted. For a local_storage result, the CLI prints a clear explanation to stderr and the complete structured JSON response to stdout. This keeps the result script-consumable instead of dropping entries that require client-side application. Pass --json to suppress the explanation and emit only JSON. In the web app, open a workspace preview and select Open logged-in preview (the key icon) in the preview toolbar. Aether mints on each click and opens a returned bootstrap URL in a new tab. If the hook returns local_storage, the web app explains that the current flow cannot apply it across origins and directs you to the CLI JSON instead.
A workspace created before authenticated previews shipped does not have the internal receiver route. The API and CLI report workspace predates preview auth; restart it. Restart the workspace to pick up the current workspace-service image; Aether never hides this as a generic server error.
Authenticated links require an authenticated Aether user who can read the workspace’s owning organization. They are never added to the sticky pull-request preview comment or any other GitHub-bound surface; PR comments continue to contain only ordinary preview links.

Supabase template

The Aether repository’s scripts/preview-auth.ts is the canonical Supabase example. It strictly parses the hook input, uses the Supabase password grant through the supplied or canonically derived 54321 sibling preview, validates the bounded response, and emits a 5173 URL fragment that supabase-js consumes:
.aether/environment.json
The session-minting core of that maintained template is:
scripts/preview-auth.ts
The fallback derives the 54321 sibling by replacing only the port prefix of a canonical preview origin. This covers services that are reachable through the preview gateway but are not declared or discovered when the hook runs. Copy the complete canonical script rather than the excerpt: its strict origin checks, sibling-origin derivation, output bounds, and password-grant response validation are part of the security boundary. Replace only the seeded development credentials and publishable key for your local Supabase stack.

Sleeping Workspaces

Idle workspaces suspend to save resources, but preview links stay valid. Opening a link to a suspended workspace lands on a start page; once the workspace is ready, the page redirects to the preview automatically. Actively using a preview also keeps its workspace awake — preview traffic defers idle suspension in 15-minute windows (up to a hard ceiling, so an abandoned tab can’t keep a VM alive forever). Waking a workspace requires its owner. Anonymous access works only while the workspace is running: the start page asks visitors to sign in, and the start action is scoped to the workspace’s owner — someone you shared a link with cannot wake your workspace. If the workspace is already starting when the page loads, no click is needed; the page waits and redirects on its own. Declare a port on a terminal in .aether/environment.json and Aether maintains a sticky Preview comment on the task’s pull request:
The comment lists each declared service as name → url and is edited in place as the task progresses — one comment per task, never a stream of new ones. Removing the declarations clears it. Reviewers can click straight through to the running app while the workspace is awake; if it has suspended, the link lands on the start page, and waking it is up to the workspace’s owner. The declared port is the contract: the link is published whether or not the service is currently listening, the same way deploy tooling posts your deploy URL regardless of your app’s health. A link to a declared port with nothing listening yet returns the gateway’s connect error until the service is up.

How Routing Works

Previews are served by the gateway proxy, separate from the main API. For each request it:
  1. Parses the subdomain into port, workspace ID prefix, and token. A malformed subdomain returns 400.
  2. Looks up a ready workspace by its ID prefix. On a hit, the token is validated with a constant-time comparison — a mismatch returns 403 — and the request is proxied to the workspace’s private IP on the requested port. Preview traffic also records activity, which keeps the workspace awake.
  3. If no ready workspace matches, the gateway checks whether the workspace exists at all (matching both prefix and token). If it doesn’t, the request returns 404.
  4. If the workspace exists but isn’t ready (suspended or starting), a browser navigation is redirected (302) to the start page — where the workspace’s owner can wake it; non-navigation requests (assets, XHR, a reloading HMR client) return 503 instead of an HTML page.

Port Discovery

Aether watches for listening ports inside the workspace and emits open/close events to the web app, so a preview becomes reachable as soon as a server starts — no configuration needed. See the ports WebSocket channel.

Host Header Rewriting

Before forwarding, the gateway rewrites the Host header to localhost:{port}. Dev servers that validate the host (Vite’s default config, for example) accept the request without extra configuration.

WebSocket Support

WebSocket upgrade requests are proxied through to the workspace, so:
  • Hot Module Replacement (HMR) works through the proxy for Vite, Next.js, and webpack dev servers
  • WebSocket-based apps (chat, live dashboards) work without modification

CORS Headers

The gateway adds CORS headers to preview responses, which enables embedding previews in iframes and making cross-port requests between previews (a frontend preview calling a backend preview on another port).

Preview Runtime

The gateway serves a small JavaScript runtime at /_aether/preview-runtime.js and injects it into proxied HTML responses. To keep injection deterministic, HTML responses are proxied uncompressed.

Troubleshooting