# Porturi și preview-uri (/ro/docs/environments/ports-and-previews)

Ajungi la un server de dezvoltare din mediu printr-un URL de preview sau o redirecționare locală de port și decizi cine îl poate deschide.

Orice port care ascultă în mediu este raportat de daemon la fiecare 15 secunde. Nimic nu e accesibil din exterior până nu îl **publici** cu o vizibilitate; abia atunci primește un URL de preview pe `preview.codai.ro` și poate fi redirecționat pe mașina ta. Tot traficul circulă prin WebSocket-ul de ieșire al daemonului — mediul nu deschide niciodată un port de intrare.

## URL-uri de preview [#url-uri-de-preview]

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

`slug` este slug-ul mediului, unic la nivel global (3–40 de litere mici, cifre sau cratime). Relay-ul face proxy HTTP/1.1 către `127.0.0.1:{port}` din mașină, păstrează `Host`, adaugă `X-Forwarded-For/Proto/Host` și transmite răspunsul nemodificat (cookie-uri, CSP, cache), plus un header `X-Codai-Environment`.

WebSocket-urile funcționează, deci și hot reload-ul: Vite are nevoie de `server.allowedHosts: ['.preview.codai.ro']`; Next.js acceptă host-ul redirecționat așa cum este. Un upstream care vorbește el însuși HTTPS (`{port}s-{slug}`) nu este încă suportat și răspunde `501 https_upstream_not_supported`.

## Vizibilitate [#vizibilitate]

| Vizibilitate | Cine poate deschide portul |
| --- | --- |
| `owner` | Doar proprietarul mediului. **Aceasta este valoarea implicită pentru orice port pe care nu l-ai publicat.** |
| `org` | Proprietarul și membrii organizației mediului. |
| `authenticated` | Proprietarul și fiecare membru al **acestui mediu** (orice rol) — nu orice utilizator codai. |
| `public` | Oricine, fără autentificare. |

Dezvoltatorii pot publica până la `authenticated`; `public` cere `maintainer` sau mai mult. Fiecare schimbare se înregistrează ca eveniment `port_visibility_changed`.

<Tabs items={['Desktop', 'CLI', 'API']}>
<Tab value="Desktop">

Vizualizarea Medii → meniul ⋯ al mediului → **Porturi**. Fiecare port care ascultă își afișează procesul și un selector de vizibilitate (*Doar eu*, *Organizație*, *Utilizatori autentificați*, *Public*), plus **Copiază URL-ul de previzualizare** și **Deschide previzualizarea**.

</Tab>
<Tab value="CLI">

```bash
codaid env ports brivio-k3xq                                   # porturi care ascultă + publicate
codaid env ports set brivio-k3xq 3000 --visibility org --label web
```

</Tab>
<Tab value="API">

```bash
curl -X PUT https://ai.codai.ro/v1/environments/$ENV_ID/ports/3000 \
  -H "Authorization: Bearer $CODAI_API_KEY" -H "Content-Type: application/json" \
  -d '{ "visibility": "org", "label": "web", "protocol": "http" }'
```

`201` pentru un rând nou, `200` la actualizare; `DELETE …/ports/3000` retrage publicarea (portul revine la `owner`).

</Tab>
</Tabs>

Hub-ul nu are încă un panou de porturi; folosește desktop-ul, CLI-ul sau API-ul.

## Deschiderea unui preview care nu e public [#deschiderea-unui-preview-care-nu-e-public]

Un browser care vizitează un preview nepublic trebuie să dovedească cine este. Relay-ul acceptă, în ordine:

1. **Un token de preview** în `?token=` sau în header-ul `X-Preview-Token` — legat de un singur mediu și un singur port, valabil implicit 60 s (până la 24 h). Acordă ce putea vedea persoana care l-a generat.
2. **Cookie-ul `codai_preview`** pe `.preview.codai.ro` (12 h) — conține doar identitatea ta; vizibilitatea se verifică din nou la fiecare cerere.
3. **`Authorization: Bearer codai_…`** — pentru scripturi și clienți nativi.

Cea mai simplă cale este un link cu token:

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

sau direct la relay:

```bash
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 }'
```

```json
{ "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.…" }
```

Pentru a seta cookie-ul, un client apelează `GET https://preview.codai.ro/v1/environments/{id}/preview-cookie` cu o cheie bearer (opțional cu `?redirect=` către un URL de preview din același domeniu, pentru un `302`). Doar proprietarul și membrii pot genera cookie-uri sau tokenuri; ceilalți primesc `404`.

Un vizitator fără credențiale primește JSON, nu o redirecționare la pagina de autentificare:

| Status | `code` | Semnificație |
| --- | --- | --- |
| `401` | `preview_auth_required` | Port nepublic și niciun token, cookie sau cheie valide. |
| `403` | `preview_forbidden` | Ești autentificat, dar vizibilitatea te exclude. |
| `404` | `environment_not_found` | Niciun mediu cu acel slug (un slug nou poate avea nevoie de până la 30 s ca să fie recunoscut). |
| `404` | `bad_preview_host` | Hostname-ul nu se potrivește cu `{port}-{slug}.preview.codai.ro`. |
| `503` | `environment_offline` | Daemonul nu e conectat (`Retry-After: 5`). |
| `503` | `env_on_other_instance` | Daemonul e pe altă instanță de relay (`Retry-After: 2`); reîncearcă. |
| `429` | `max_streams` | Peste 256 de fluxuri deschise pe acest mediu. |
| `502` | `upstream_error` | Nimic nu a răspuns pe port — de obicei serverul încă nu ascultă. |

## Redirecționează un port pe mașina ta [#redirecționează-un-port-pe-mașina-ta]

```bash
codaid env forward brivio-k3xq 5432 --local 15432
# 127.0.0.1:15432 → 5432 în mediu
psql "postgres://app@127.0.0.1:15432/app"
```

Fiecare conexiune TCP locală devine un flux prin relay, supus aceleiași verificări de vizibilitate (un port refuzat se închide cu `port_forbidden`). Merge orice TCP simplu — baze de date, Redis, orice. Aplicația desktop nu are încă o interfață de redirecționare.

## Limite [#limite]

| Limită | Valoare |
| --- | --- |
| Fluxuri deschise per mediu | 256 |
| Date netrimise per flux | 256 KB, apoi fluxul se închide cu `backpressure` |
| Flux TCP/HTTP inactiv | 10 min (terminalele nu expiră niciodată) |
| Cea mai lungă conexiune | 1 oră — timeout-ul de cerere al relay-ului; clienții se reconectează |
| Căderea daemonului | fluxurile deschise se închid la 30 s după deconectarea daemonului; nu sunt reluate |

Tailscale ca rută directă opțională este planificat; azi tot traficul trece prin relay.
