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.roslug 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
| Visibility | Who may open the port |
|---|---|
owner | The environment owner only. This is the default for any port you have not published. |
org | The owner and members of the environment's org. |
authenticated | The owner and every member of this environment (any role) — not every codai user. |
public | Anyone, 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:
- A preview token in
?token=or theX-Preview-Tokenheader — 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. - The
codai_previewcookie on.preview.codai.ro(12 h) — it carries only your identity; visibility is checked again on every request. 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:
| Status | code | Meaning |
|---|---|---|
401 | preview_auth_required | Non-public port and no valid token, cookie or key. |
403 | preview_forbidden | Signed in, but the visibility excludes you. |
404 | environment_not_found | No environment with that slug (a new slug can take up to 30 s to resolve). |
404 | bad_preview_host | The hostname does not match {port}-{slug}.preview.codai.ro. |
503 | environment_offline | The daemon is not connected (Retry-After: 5). |
503 | env_on_other_instance | The daemon is on another relay instance (Retry-After: 2); retry. |
429 | max_streams | More than 256 open streams on this environment. |
502 | upstream_error | Nothing 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
| Limit | Value |
|---|---|
| Open streams per environment | 256 |
| Unsent data per stream | 256 KB, then the stream closes with backpressure |
| Idle TCP/HTTP stream | 10 min (terminals never idle out) |
| Longest single connection | 1 hour — the relay's request timeout; clients reconnect |
| Daemon drop | open 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.
Managed environments
VMs that codai provisions for you on OpenStack or Hetzner — sizes, regions, start and stop, idle auto-stop and snapshots.
Team environments
Share one environment with your team — roles, a Linux account and port range per member, a shared repository, the build lock and isolation modes.