Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Preview relay

The preview relay lets you open a dev server that runs on one of your machines in a browser anywhere, without a VPN and without opening a port. Your helper dials out to an aya-relay, the hub decides who may open what, and the relay serves the dev server at an address of its own:

https://5173-kg724cafc4b2t3hz.preview.example.test

5173 is the port the dev server listens on, and kg724cafc4b2t3hz is a label derived from the worker’s identity. The relay needs its own domain, the hub mints short-lived tickets, and nothing about your machine is reachable except the ports the helper agrees to forward.

Examples use hub.example.test, relay.example.test and preview.example.test. The wire protocol is docs/relay/protocol.md in the repository, and the security design is docs/relay/threat-model.md.

Use it

  1. Your operator runs a relay and turns it on in the hub (Run your own: the preview relay).

  2. Start the helper from a build with the relay cargo feature and add a [relay] section to its config:

    [relay]
    url = "wss://relay.example.test"
    resource = "https://relay.example.test"
    

    The helper reuses its OIDC client credentials to request the relay:connect scope on that resource. Use one OIDC client per worker: two helpers sharing a client share a label and would keep replacing each other. The helper keeps its relay instance id in its data directory (relay-instance-id), so a restarted helper is the same instance to the relay.

  3. Declare the dev server in the helper config with [[relay.services]] (port = 5173) on an allowed port (default 3000-3999, 4173, 5173-5199 and 8000-8099), and start it. Set discover = true under [relay] to offer every listening allowed port instead. Harness sessions on that helper get AYA_PREVIEW_URL, AYA_PREVIEW_HOST, AYA_PREVIEW_DOMAIN and __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS in their environment, so a tool can print the right address.

  4. Open https://hub.example.test/api/relay/open?worker=<label>&service=http:5173 while signed in to the hub. The hub checks that you may open it and hands your browser to the preview, which sets its own session cookie and sends you to /. Visiting the preview address directly sends you to the same hub route. If you are not signed in, the hub asks you to sign in first and does not remember where you were going, so open the preview address again after signing in.

Most dev servers need one setting to accept the preview host; see the dev server notes below.

Share a preview

POST /api/relay/shares (signed in) creates a link to one worker’s service that someone else can open:

curl -sS -X POST https://hub.example.test/api/relay/shares \
  -H 'Content-Type: application/json' -b cookies.txt \
  -d '{"worker":"kg724cafc4b2t3hz","service":"http:5173","ttl_secs":3600,"audience":"org"}'

The response carries url (https://hub.example.test/r/s/<token>), id and expires_at (unix seconds). audience is org (the visitor signs in to the hub first) or anyone. anyone is refused unless the operator set RELAY_ANONYMOUS_SHARES=1. Visitors get a session of at most 12 hours, and never longer than the share. GET /api/relay/shares lists shares (it never shows the token again) and DELETE /api/relay/shares/{id} revokes one; the relay applies a revocation within about 30 seconds. Both only see shares the caller created; revoking someone else’s share answers 404. A hub operator (see RelayPolicy::is_operator; on the single-owner hub every user is one) can add ?all=true to the listing and can revoke any share.

Open a preview from a script

A machine caller (for example a cloud agent that has to look at the page) uses the hub’s bearer route, with the same client-… credentials the Remote API uses:

curl -sS -X POST https://hub.example.test/api/remote/v1/relay/tickets \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"worker":"kg724cafc4b2t3hz","service":"http:5173","cap":["open"]}'

The route is a remote API scope: the token needs relay:open on the remote API resource, its subject must pass the remote API allowlist and must not be deprovisioned, and each ticket costs one remote read from the same per-principal budget as the remote API’s reads (429 with Retry-After when it is spent). The subject must also be in RELAY_API_ALLOWED_SUBJECTS (the route is closed until that is set). The answer is {ticket, url, expires_at}: send the ticket to the preview host as Aya-Relay-Authorization: Bearer <ticket> on each request. A bearer ticket lives 5 minutes. The operation is in the OpenAPI document under the relay tag.

A ticket with control on a stream: service carries a control lease that the hub mints and returns as lease. Send that value as lease on the next ticket request to keep the same lease. The hub refuses a lease it did not mint, a lease minted for another subject or worker, and a revoked lease. After an operator revokes a control lease, the hub also refuses that subject a new control lease on that worker for one hour; the subject can still view, and keeps control of its other workers. To keep a subject away for longer, revoke the subject or deprovision the principal.

Reservation workers

A worker the hub enrolled for a reservation (its subject starts with client-w-) authenticates to the relay with tokens from the hub’s own workload issuer, and its label is derived from that issuer. Only the principal that requested the reservation may open it, and only through the bearer route above: a browser session gets 403, whoever is signed in. When the reservation ends, nobody can open the worker.

An enrolled helper mints its relay token from its own workload key for the relay’s audience and never uses the machine’s OIDC client. A broken [relay] section disables the relay on that worker and is logged; it never stops the worker.

Revocation

Revoking a share, session, worker, subject or control lease reaches every relay replica through the hub’s revocation feed. The hub also adds entries itself: disabling a worker client revokes that worker, and deprovisioning a principal revokes that subject. A subject the hub deprovisioned gets no new ticket on any route, and for one hour after a subject revocation the hub mints that subject nothing. When a viewer logs out of a preview, the relay reports it and the session ends on every replica.

Dev servers

The relay forwards plain HTTP and WebSocket traffic and never rewrites bodies. The dev server sees a request for <port>-<label>.<preview-domain> and must be willing to answer it:

  • Vite: server.allowedHosts (the helper already sets __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS to .preview.example.test) and server.hmr = { clientPort: 443, protocol: 'wss' }.
  • Next.js: list the host in allowedDevOrigins.
  • Create React App and webpack-dev-server: allowedHosts: 'all' (or the host) and client.webSocketURL pointing at port 443 over wss.
  • If a server insists on localhost, set host_mode = "loopback" for its port under [[relay.services]]. The worker then rewrites Host and Origin to localhost:<port>.
  • Absolute http://localhost:… URLs inside pages and bundles are not rewritten and will fail in the visitor’s browser. Keep them relative.

docs/relay/dev-servers.md has the full recipes and a troubleshooting table.

Limits worth knowing

  • Only http services open in a browser in v1; shares are HTTP only.
  • A visitor session ends after at most 12 hours. The relay checks the hub for revocations every 30 seconds, so revoking is not instant. The hub drops feed entries the relay no longer needs (sessions, leases and shares after 13 hours, workers after 2 hours).
  • With a plain-http hub URL on another machine the helper cannot fetch ticket keys from the hub, so it refuses stream: services; set ticket_jwks_url under [relay] to an https key URL to enable them. A hub on the same machine (http://127.0.0.1, http://localhost) keeps working over plain http.
  • The preview domain should be on the Public Suffix List, or dedicated to one organization. Otherwise sibling previews share cookies. See docs/relay/threat-model.md.
  • Reservation workers open only for their requester. Every other worker is open to every signed-in hub user, so enable the relay for those only on a hub with one human user.