codai docs
Projects & Environments

Ports and previews

Reach a dev server inside an environment through a preview URL or a local port forward, and decide who may open it.

Every port that listens inside an environment is reported by the daemon every 15 seconds. Nothing is reachable from outside until you publish it with a visibility; then it gets a preview URL on preview.codai.ro and can be forwarded to your machine. All traffic travels over the daemon's outbound WebSocket — the environment never opens an inbound port.

Preview URLs

https://{port}-{slug}.preview.codai.ro/
        3000-brivio-k3xq.preview.codai.ro

slug is the environment's globally unique slug (3–40 lowercase letters, digits or dashes). The relay proxies HTTP/1.1 to 127.0.0.1:{port} inside the machine, preserves Host, adds X-Forwarded-For/Proto/Host, and passes the response through unchanged (cookies, CSP, caching) plus an X-Codai-Environment header.

WebSockets work, so hot reload works: Vite needs server.allowedHosts: ['.preview.codai.ro']; Next.js accepts the forwarded host as is. An upstream that itself speaks HTTPS ({port}s-{slug}) is not supported yet and answers 501 https_upstream_not_supported.

Visibility

VisibilityWho may open the port
ownerThe environment owner only. This is the default for any port you have not published.
orgThe owner and members of the environment's org.
authenticatedThe owner and every member of this environment (any role) — not every codai user.
publicAnyone, without signing in.

Developers may publish up to authenticated; public needs maintainer or higher. Every change is recorded as a port_visibility_changed event.

Environments view → the environment's ⋯ menu → Ports. Each listening port shows its process and a visibility dropdown (Only me, Organisation, Signed-in users, Public), plus Copy preview URL and Open preview.

The hub does not have a port panel yet; use the desktop, the CLI or the API.

Opening a non-public preview

A browser visiting a non-public preview must prove who it is. The relay accepts, in order:

  1. A preview token in ?token= or the X-Preview-Token header — bound to one environment and one port, valid 60 s by default (up to 24 h). It grants what the person who minted it could see.
  2. The codai_preview cookie on .preview.codai.ro (12 h) — it carries only your identity; visibility is checked again on every request.
  3. Authorization: Bearer codai_… — for scripts and native clients.

The simplest route is a token link:

codaid env preview brivio-k3xq 3000 --ttl 600
# https://3000-brivio-k3xq.preview.codai.ro/?token=codai_pv1.…

or directly against the relay:

curl -X POST https://relay.codai.ro/v1/environments/$ENV_ID/preview-token \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "port": 3000, "ttl_s": 600 }'
{ "token": "codai_pv1.…", "expires_at": "2026-09-25T10:10:00.000Z", "ttl_s": 600, "port": 3000,
  "host": "3000-brivio-k3xq.preview.codai.ro", "url": "https://3000-brivio-k3xq.preview.codai.ro/?token=codai_pv1.…" }

To set the cookie instead, a client calls GET https://preview.codai.ro/v1/environments/{id}/preview-cookie with a bearer key (optionally ?redirect= to a preview URL on the same domain for a 302). Only the owner and members can mint cookies or tokens; others get 404.

A visitor without credentials gets JSON, not a login redirect:

StatuscodeMeaning
401preview_auth_requiredNon-public port and no valid token, cookie or key.
403preview_forbiddenSigned in, but the visibility excludes you.
404environment_not_foundNo environment with that slug (a new slug can take up to 30 s to resolve).
404bad_preview_hostThe hostname does not match {port}-{slug}.preview.codai.ro.
503environment_offlineThe daemon is not connected (Retry-After: 5).
503env_on_other_instanceThe daemon is on another relay instance (Retry-After: 2); retry.
429max_streamsMore than 256 open streams on this environment.
502upstream_errorNothing answered on the port — usually the server is not listening yet.

Forward a port to your machine

codaid env forward brivio-k3xq 5432 --local 15432
# 127.0.0.1:15432 → 5432 in the environment
psql "postgres://[email protected]:15432/app"

Each local TCP connection becomes one stream through the relay, subject to the same visibility check (a refused port closes with port_forbidden). Plain TCP works — databases, Redis, anything. The desktop app has no forward UI yet.

Limits

LimitValue
Open streams per environment256
Unsent data per stream256 KB, then the stream closes with backpressure
Idle TCP/HTTP stream10 min (terminals never idle out)
Longest single connection1 hour — the relay's request timeout; clients reconnect
Daemon dropopen streams close 30 s after the daemon disconnects; they are not resumed

Tailscale as an optional direct path is planned; today all traffic goes through the relay.

On this page