---
title: TCP edge
description: Protocol ports, implemented TLS routing, access grants, and the reconnect contract for private services.
audience: everyone
status: implemented
last_updated: 2026-09-29
order: 20
related:
  - /v/docs/concepts/sites-and-deploys.md
  - /v/docs/getting-started/quickstart.md
  - /v/docs/ui/building-an-image.md
  - /v/docs/api/index.md
  - /v/docs/llm.md
---

This page defines the implemented edge contract. Manifest additions, managed
TLS delivery, Supervisor relays, protocol policy, private APIs, Envoy/xDS, and
the MySQL gateway exist in source with component and local integration tests.
The port map and client examples do not establish public deployment or verified
connectivity.
Check the returned endpoint and protocol readiness before attempting a connection.

## Four external ports

Ports identify protocols, never tenants. Every tenant shares the applicable
protocol port; SNI hostnames or platform-minted credentials select the tenant.
No additional external TCP port is permitted. Runtime ports and internal Unix
sockets are independent of this map.

| External port | Protocol | Tenant selection and TLS |
| --- | --- | --- |
| 443 | HTTPS; Redis/Valkey TLS; MongoDB TLS; other client-first TLS protocols | TLS ClientHello SNI selects the route. Private TCP TLS passes through to the runtime. HTTP continues through the HTTP routing and access layer. |
| 5432 | PostgreSQL | SSLRequest-aware negotiation followed by SNI selection; the stream passes through byte for byte, so TLS exists only when the database itself has `ssl` enabled. |
| 3306 | MySQL and MariaDB | A peer MySQL gateway handles the server-first handshake, requires TLS, authenticates platform-minted credentials, and routes by the site-unique username. |
| 5671 | AMQPS | Client-first TLS and SNI selection; TLS passes through to the runtime. |

External application traffic uses TLS only. Plaintext RESP, AMQP, and database
sessions are never externally offered. A schema protocol value alone does not
prove TLS capability: for example, a MongoDB service without server TLS remains
internal-only. There is no external plaintext Redis listener or per-tenant port.

## Routing mechanisms

### Client-first TLS through Envoy

Envoy inspects the ClientHello on 443 and 5671, chooses an SNI filter chain, checks
the source allowlist, and forwards the unmodified TLS stream to the selected
Supervisor endpoint relay. xDS supplies endpoint and grant state. Missing or
unknown SNI and unauthorized sources fail closed before wake. Use the assigned
service hostname rather than an IP address; verify its certificate on the client.

### PostgreSQL SSLRequest-aware negotiation

PostgreSQL normally sends an eight-byte SSLRequest before a TLS ClientHello.
The Envoy C module consumes that negotiation before SNI selection, then sends
an independent SSLRequest on the backend leg; a generic TLS inspector alone
is insufficient. The SSLRequest is negotiation,
not permission to skip authentication. Database credentials are always
required; TLS exists only when the database itself has `ssl` enabled, because
the edge relays the stream byte for byte and never terminates it. A client
without SNI cannot select a tenant on the shared listener.

### MySQL gateway

MySQL sends its server greeting before the client sends a TLS ClientHello.
The gateway on 3306 therefore terminates client TLS, verifies platform-minted
credentials, and selects the endpoint by the unique routing username. It is a
peer of Envoy, not an Envoy listener. The private backend is plaintext by design;
`protocol: mysql` rejects the platform backend `tls: {}` marker.

MySQL/MariaDB/WordPress template defaults render `u_{{ site.token }}` using the persisted
site ID. Connect with that resolved username from the owner connection panel,
not a shared `app` username or the literal placeholder. Credential generation, gateway snapshot consumption, and revocation enforcement
are implemented and tested. Public listener activation still requires deployment
verification. Custom usernames
require a valid unique gateway binding before external access can be enabled.

## Grants and allowlists

The current owner API accepts `POST /sites/{slug}/tcp-access` with a non-public
manifest service name and a duration of 1–1440 minutes. It derives the source IP
from the request and returns `hostname`, `port`, `connection`, `cidr`,
`expires_at`, `operation_id`, and `grant_id`, plus `edge_state`,
`protocol_state`, `protocol`, and nullable `routing_username`. Connect only from
the reported CIDR. A grant is network authorization; it does not replace
database authentication or TLS verification.

Source responses use `mysql://<routing_username>@<hostname>:3306` for MySQL and
`tcp://<hostname>:<port>` for other protocols. They report
`configured_unverified` / `unverified`; allocation or configuration is not a
successful protocol probe. The source policy selects `services.<name>.protocol`,
uses the fixed port map above, and retains the legacy private `database`@5432
inference for compatibility. `tcp`, `redis`, `mongodb`, and `amqp` require the
`tls: {}` marker for an external grant. Runtime TLS capability must still be
verified. Service names must also form a safe endpoint identity for provisioning.

Omitting protocol is intended to leave a new private service internal-only.
Current validation normalizes omission to `tcp`; the policy can admit that
normalized service if it has `tls: {}`. Omission alone therefore does not prove
isolation. Declare the protocol explicitly and withhold grants for internal
services. Strict omitted-protocol isolation remains a policy gap.

Envoy/gateway admission and Supervisor relay checks enforce
grant CIDRs and expiry before wake. Revocation and credential rotation invalidate cached authorization. The relay bounds established connections by
the grant deadline. HTTP `public` / `authenticated` access is a separate policy.

### Private control-plane interfaces — implemented in source

These routes are outside `/v/api/v1`, use dedicated internal bearer credentials,
and are not customer or agent PAT interfaces:

| Method and route | Contract |
| --- | --- |
| `GET /_internal/edge/state` | Full endpoint replacement snapshot: protocol, external port/mechanism, node, `socket_path` / `internal_port`, PROXY-v2 requirement, grant CIDRs/expiry, `draining`, and `available`. |
| `POST /_internal/edge/authorize` | Accepts `source_ip`, `sni`, and `listener_port`; returns `allowed`, endpoint identity, and a bounded authorization lifetime. Denies stale, draining, missing, or unauthorized routes without waking them. |
| `POST /_internal/edge/endpoints/{endpoint_id}/drain` | Marks draining and increments the revision. It does not itself withdraw an Envoy cluster, quiesce a relay, or checkpoint. |
| `GET /_internal/gateway/credentials` | Full credential replacement snapshot: username, password verifier, endpoint/socket identity, revocation state, and active grant deadlines. The default response is verifier-only; an explicit HTTPS-only `include_backend_password=1` pull supplies the password required for independent backend authentication. |

Edge state/authz/drain require `SITE_EDGE_API_TOKEN`; credential sync requires
`SITE_MYSQL_GATEWAY_API_TOKEN`. Empty or incorrect tokens fail closed. Snapshots
include `replace: true`, a content revision, `cache_ttl_seconds`, and
`valid_until`; consumers must evict absent entries and expired snapshots.
The configured TTL is capped at five seconds and bounded by grant expiry.
Rotation, expiry, drain, and purge affect the next snapshot. The gateway evicts
stale/failed snapshots and revokes sessions. Envoy removes draining resources;
controller ACK and connection grace-period checks must precede Supervisor
quiescence in the lifecycle coordinator.

## Connect with protocol clients

These examples apply only after the relevant edge stage and endpoint have been
verified. Set `TCP_HOST` to the returned service hostname and `CA_FILE` to a
trusted CA bundle. Use the owner-revealed credentials; never send a control-plane
PAT to an application endpoint. Keep passwords out of URLs and shell history.

| Protocol | Client configuration |
| --- | --- |
| HTTP | Open `https://<site>.<namespace>.serverl3ss.com` on 443; follow the site's HTTP access policy. |
| PostgreSQL | Use the service hostname, 5432, and SNI. The stream passes through byte for byte: enable `ssl` with the delivered service certificate on the database for TLS; the seeded PostgreSQL template currently serves plaintext, so `sslmode=prefer` negotiates down. |
| MySQL/MariaDB | Use 3306, the resolved routing username, and `VERIFY_IDENTITY`. |
| Redis/Valkey | Use 443, TLS, SNI, and ACL user `app`; the URI scheme is `rediss`. |
| AMQP | Use `amqps://<username>@<service-host>:5671` with hostname verification and application credentials. |
| MongoDB / generic TCP | Use 443 only with client-first TLS, SNI, certificate verification, and an approved binding; otherwise keep the service internal. |

```bash
psql "host=$TCP_HOST port=5432 dbname=app user=app"
# With ssl enabled on the database:
psql "host=$TCP_HOST port=5432 dbname=app user=app sslmode=verify-full sslrootcert=$CA_FILE"
mysql --host="$TCP_HOST" --port=3306 --user="$ROUTING_USERNAME" \
  --ssl-mode=VERIFY_IDENTITY --ssl-ca="$CA_FILE" --password app
redis-cli --tls --sni "$TCP_HOST" --cacert "$CA_FILE" \
  -h "$TCP_HOST" -p 443 --user app --askpass PING
```

MariaDB clients must use their equivalent CA and server-certificate verification
options. A Redis/Valkey server must enable its TLS port, disable plaintext
(`--port 0`), load its service certificate/key, and authenticate the `app` ACL
user. The seeded ACL disables `default` and removes `@dangerous` commands.

## Reconnect and awake time

**Client connections do not survive instance recycle.** Checkpoint/restore,
snapshots, restart, node moves, and idle scale-to-zero require clients to
reconnect. Configured durable state persists, but a socket and an in-flight
transaction are not durable state. Use connection-pool retries with bounded
backoff and resolve ambiguous write outcomes before retrying.

**Client keepalives hold an instance awake; that awake time is billable.** Tune
pool lifetime, idle connections, and keepalives to the workload's cost and
latency needs. Supervisor relays own connection idle timeouts. Edge active
health checks against wake-gated runtimes are forbidden because they would
continually wake the workload.

## Implementation and metering status

| Stage | Repository status |
| --- | --- |
| Protocol/TLS manifest fields and template defaults | Implemented in the validator and seed source; existing database rows need the normal seed rollout. |
| Internal per-endpoint Supervisor relays | Implemented in source: trusted-peer PROXY v2 admission, wake, dial, relay, connection limits, and byte events. |
| Managed per-service TLS delivery | Issuance and delivery implemented/tested for gVisor mounts, Firecracker disk material, and K3s Secrets; real Redis TLS integration coverage. Public deployment must be verified separately. |
| Protocol policy, internal endpoint persistence, edge state/authz/credential APIs | Implemented in source with additive migration `0100_edge_endpoint_access_policy`; edge/gateway consumers are implemented; public deployment must be verified separately. |
| Network byte ingestion | Implemented in source; scoped to node/endpoint and deduplicated by node, Supervisor stream, and sequence. Public site usage exposes integer `network_bytes` totals. |
| Envoy/xDS and MySQL gateway | Implemented and component-tested, including optional real Envoy/libpq and MySQL server/CLI oracles. Envoy is absent from the 3306 path. |
| Public four-port activation | Deployment/drain source and repeatable protocol harnesses exist; this document does not establish an activated public rollout. |

The Supervisor is the authoritative billing meter. Edge counters are for
reconciliation only and must not independently charge usage. Relay source emits
`network_bytes` with received/sent byte counts. Django ingestion records their
sum as bytes with directional counters and duration in ledger metadata, once
per node/stream/sequence. Its metered event set includes `wake_minutes`,
`requests`, `storage_gb_hours`, and `network_bytes`. Ingestion is a recorded fact,
not proof of deployment, complete coverage, or a new pricing rule. The public site usage response includes `network_bytes` as an integer.

This page supersedes the historical dedicated-port and TCP pool designs.
