Relay & environments

A control plane outside the normal traffic path

The relay is T3 Connect's hosted control plane. It links accounts to environments, provisions endpoints, and issues short-lived credentials. After connection, normal API and WebSocket traffic goes directly between the client and environment.

client DPoP proof key relay worker T3CodeRelay stack environment https + wss endpoint link proof + challenge 2 min mint proof direct API + WebSocket after connect the relay is no longer in this path
pairing: client asks the relay to link and mint, then connects straight to the environment

The relay establishes trust but does not carry normal traffic

The relay is the hosted control plane for T3 Connect. It helps clients discover and connect to remote environments, manages the cloud-side records those connections need, and delivers optional mobile notifications and Live Activities (infra/relay/README.md:6-8). It stores control-plane decisions and records: which accounts and environments are linked, which endpoint belongs to each environment, and which credentials remain valid.

Its defining constraint is a refusal. "The relay is intentionally not in the hot path for normal T3 Code traffic. After a client connects, regular API and WebSocket traffic goes directly between that client and the selected environment" (infra/relay/README.md:10-11). The relay handles connection setup once. The client and environment then exchange sessions, streaming diffs, and chart payloads directly.

The second boundary is credential separation. The environment server and the relay hold "separate credentials and trust boundaries" (infra/relay/README.md:27-29), a split written down in detail in docs/internals/environment-auth.md. The relay can establish the connection, but it cannot use an environment credential after setup.

Back to the big picture to see where this plane sits beside the event spine and the exchange.

Short-lived everything

Every credential in this boundary is measured with a clock. The relay's client access tokens are DPoP-bound: each carries a cnf.jkt proof-key thumbprint (infra/relay/src/auth/RelayTokens.ts:183), so a stolen token cannot be replayed without the matching private key. The docs put it plainly: relay-brokered clients use this mode "so that a leaked token cannot be replayed without the corresponding key" (docs/internals/environment-auth.md:91-93).

2min
relay to environment mint and health proofs
EnvironmentConnector.ts:420,575
5min
single-purpose WebSocket ticket
environment-auth.md:95-103
30min
DPoP-bound client access token
RelayTokens.ts:29
1hour
DPoP-bound environment session; a plain bearer one lasts 30 days
environment-auth.md:71-74
t3-relay-dpop-access+jwt
Client access token for relay APIs. TTL is a named constant, RELAY_DPOP_ACCESS_TOKEN_TTL = "30 minutes". Scopes are capped per client: mobile may hold environment:connect, environment:status, and mobile registration; web gets connect and status only (infra/relay/src/auth/RelayTokens.ts:29,63-73).
t3-link-challenge+jwt
Linking starts with a relay-signed challenge carrying the requested capabilities (notifications, Live Activities, managed tunnels) as booleans; the environment answers with the link proof (infra/relay/src/auth/RelayTokens.ts:26-42,122-139).
mint / health proof
When a client asks for status or connect, the relay signs a JWT for the environment: audience t3-env:<environmentId>, scope environment:status or environment:connect, valid for 2 minutes. The environment answers with its own signed proof over a fresh nonce (infra/relay/src/environments/EnvironmentConnector.ts:419-464,575-611).
t3env_<credentialId>_<secret>
The environment's credential toward the relay. Ids and secret come from joined UUIDs with dashes stripped; only a SHA-256 base64url hash of the whole token is stored, and creating a new one revokes every predecessor for the same environment and public key (infra/relay/src/environments/EnvironmentCredentials.ts:87-105,151-175).
wsTicket
A five-minute, single-purpose ticket minted by POST /api/auth/websocket-ticket. The client authenticates with its real credential in headers, then puts only the ticket in the socket URL, so long-lived tokens and cookies never appear in a URL (docs/internals/environment-auth.md:95-103).
environment session
The long end of the scale, and still bounded: 30 days for a plain bearer exchange, one hour when DPoP-bound. Scope requests must be a subset of the bootstrap grant, so an ordinary paired client cannot exchange its way into admin scopes (docs/internals/environment-auth.md:59-78).

On the way in, the relay itself verifies two kinds of bearer credential: Clerk template JWTs generated from the t3-relay template with the shared t3-code-relay audience, and Clerk OAuth tokens issued to the CLI. verifyRelayClientBearerToken tries the template path first and falls back to OAuth (docs/internals/t3-connect.md:6-11). Its protected-resource metadata then advertises the contract: token endpoint /v1/client/dpop-token, grant type token-exchange, ES256 signing, and dpop_bound_access_tokens_required: true (infra/relay/src/http/Api.ts:355-385).

One stack, four moving parts

The relay deploys as a single Alchemy stack named T3CodeRelay, built from the Axiom, Cloudflare, and Drizzle providers. It yields the schema snapshot, the Hyperdrive binding, the managed endpoint and API DNS zones, the observability resources, and the Worker itself (infra/relay/alchemy.run.ts:15-28).

Cloudflare Worker

infra/relay/src/worker.ts wires the Cloudflare bindings, runtime layers, queues, and HTTP API groups: health, metadata, mobile, client, token, dpop, and server handlers (infra/relay/README.md:31-43).

Two queues ride along: an APNs delivery queue and its dead-letter partner. Batch and message processing run under spans named relay.apn_delivery_queue.process_batch and relay.apn_delivery_queue.process_message (infra/relay/src/worker.ts:122-123,245-260). Mobile push is inherited upstream surface area; this fork keeps it wired but does not develop it.

Diagnostics in Axiom

One OpenTelemetry trace dataset, t3-code-relay-traces-prod, is shared by the Worker, the mobile app, and first-party relay clients. Three dataset-scoped ingest tokens are write-only per producer, and a t3-code-relay-recent-spans-prod view holds recent request and endpoint spans (docs/operations/relay-observability.md:5-12).

Relay-specific annotations land under attributes.custom, and relay.operation is one of them (docs/operations/relay-observability.md:46-48). Responders are told to prefer the view or APL queries over tailing the Worker (docs/operations/relay-observability.md:50-53).

t3-code-relay-traces-prod recent-spans view relay.operation

Postgres behind Access

The database lives on a self-hosted origin behind Cloudflare Access, reached through a Cloudflare Tunnel. Access supplies the origin credential, so the Hyperdrive config declares no port at all; the tunnel's TCP ingress owns it (infra/relay/src/db.ts:48-52).

Local development reaches the same database through its own tunnel session onto 127.0.0.1:5432 (infra/relay/src/db.ts:77-89).

Worker Hyperdrive Tunnel Access Postgres

Caching, deliberately off

Hyperdrive can cache reads, and for this database the answer is no:

"The relay reads credentials, allocations and DPoP proofs. A stale read here is an auth bug, so this cache stays off."

That comment sits directly above caching: { disabled: true } in the Hyperdrive binding, with an origin connection limit of 20 (infra/relay/src/db.ts:71-76). The schema is Drizzle with dialect postgres and migrations under infra/relay/migrations/postgres (infra/relay/src/db.ts:42-46).

Linking: eleven gates before a yes

An environment joins an account by presenting a link-proof JWT of type RELAY_LINK_PROOF_TYP from the shared JWT helpers (infra/relay/src/environments/EnvironmentLinker.ts:6-11). The relay never trusts it wholesale; it walks a staged validation chain, and any failed stage is reported by name in the error (infra/relay/src/environments/EnvironmentLinker.ts:38-63):

decode_token decode_payload verify_proof authorize_capabilities validate_descriptor verify_challenge validate_expiration consume_proof_nonce consume_challenge_nonce validate_origin validate_endpoint

The relay checks each requested capability: requesting managed tunnels fails unless the proof carries the managed_tunnels scope, and notifications or Live Activities require agent_activity_notifications (infra/relay/src/environments/EnvironmentLinker.ts:95-107). The endpoint gate requires https for the HTTP base URL and wss for the WebSocket base URL, refusing anything else (infra/relay/src/environments/EnvironmentLinker.ts:109-117).

Only then does the relay upsert the link row, mint an environment credential, and provision the managed endpoint, returning all three results to the caller (infra/relay/src/environments/EnvironmentLinker.ts:73-91).

Endpoints from a digest

A managed endpoint is a Cloudflare tunnel with a hostname the relay derives, not one anyone picks. The same environment under the same account always maps to the same name, and production hostnames read prod-<digest>.<RELAY_TUNNEL_ZONE_NAME> while personal Alchemy stages prefix their own sanitized stage name (infra/relay/README.md:102-108).

digest input
Exactly stage:userId:environmentId, hashed into the hostname suffix (infra/relay/src/deploymentConfig.ts:88-99).
endpoint pair
Always https://<hostname>/ and wss://<hostname>/ws with providerKind: "cloudflare_tunnel" (infra/relay/src/deploymentConfig.ts:112-118).
deprovisioning
Its own six stages, from claim-release through DNS record and tunnel deletion to allocation removal (infra/relay/src/environments/ManagedEndpointProvider.ts:77-84).
connect mismatches
When a client asks to connect, the relay re-derives the endpoint and hands over a 2-minute proof. Mismatches are hard failures with named reasons: allocation not ready, hostname invalid, endpoint mismatch, or the endpoint simply not being relay-managed (infra/relay/src/environments/EnvironmentConnector.ts:45-69).
derive-environment-hash
Hash the stage, user, and environment identity.
check-tunnel-limit
Refuse early if the account is at its tunnel budget.
reserve-allocation
Claim the row that will own this endpoint.
ensure-tunnel
Create the Cloudflare tunnel if absent.
validate-tunnel-response
Check the tunnel answered with the identity we asked for.
record-tunnel
Persist tunnel id and name against the allocation.
configure-tunnel
Point ingress at the environment's origin.
ensure-dns-record
Create the hostname in the tunnel zone.
record-dns
Persist the record id for later teardown.
get-tunnel-token
Fetch the credential the environment runs cloudflared with.
mark-allocation-ready
Flip the endpoint live. Failures report their stage (ManagedEndpointProvider.ts:57-75).

Provisioning stage names verbatim from infra/relay/src/environments/ManagedEndpointProvider.ts:43-55.

Two issuers, zero interchange

Environment server and relay keep separate credentials, issuers, and trust boundaries, deliberately shaped like OAuth so permission checks can be audited against established concepts (docs/internals/environment-auth.md:5-7). The token endpoint profiles RFC 8693 token-exchange vocabulary: subject_token, requested_token_type, issued_token_type (docs/internals/environment-auth.md:113-119). It is not a general authorization server; the bootstrap token type is private and errors are typed (docs/internals/environment-auth.md:123-127).

Authorization is capability-based. Ordinary pairing links grant exactly four client-operation scopes plus read access to relay connectivity; bootstrap credentials add the three administrative ones (docs/internals/environment-auth.md:25-29). A WebSocket ticket carries its session's scopes, and every RPC method re-checks the one it needs: "Creating a ticket is not authorization to call every RPC method" (docs/internals/environment-auth.md:105-111).

The scope model was born in a hard cutover. Migration 031_AuthAuthorizationScopes deleted existing pairing links and sessions rather than silently mapping old role-bearing records to new capabilities; upgraded clients simply pair again (docs/internals/environment-auth.md:130-135).

Capability scopes

orchestration:read orchestration:operate terminal:operate review:write access:read access:write relay:read relay:write

The eight environment-side scopes (docs/internals/environment-auth.md:14-23). Pairing grants the first four plus relay:read; bootstrap adds access:read access:write relay:write.

The boundary rule

"An environment access token is not a relay token and cannot be presented to the relay." (docs/internals/environment-auth.md:139-141). Different issuers, different keys, different trust: a credential from one side is inert paper on the other.

Production shape: re-verify, do not assume

Repository code is not the complete source of truth for production. The working rule names three layers that must agree before any diagnosis or change: the repository (infra/relay and its docs), the host running the Postgres origin and cloudflared, and the live Cloudflare account with its Workers, DNS, Access, tunnels, Hyperdrive, and queues (AGENTS.md, Relay and production infrastructure).

The documented shape is a starting point that must be verified. The fork's default relay URL https://relay.athelstan.xyz and hosted app https://app.athelstan.xyz are fork-patched constants with comments warning "An upstream sync will try to revert them" and directing the reader to keep the fork's values, guarded by tests in both the web and server packages (packages/shared/src/connectAuth.ts:12-32; apps/web/src/cloud/publicConfig.test.ts:15-21; apps/server/src/cloud/publicConfig.test.ts:38-46).

Docs can lag, and this area has an example on file. The relay README's deployment section still says the prod stage "owns the retained PlanetScale database" (infra/relay/README.md:92), while the schema is Drizzle postgres with migrations under infra/relay/migrations/postgres (infra/relay/src/db.ts:42-46) and operations document a self-hosted Postgres origin behind Access. Treat repository prose as one witness among three.

Repository

infra/relay for the Alchemy stack, Worker, migrations, and contracts; docs/internals and docs/operations for design and procedure. The first witness, read-only.

The host

The origin machine runs the Postgres container and cloudflared; the database is bound to loopback, not the public network (AGENTS.md). Inspect before touching; never restart casually.

Cloudflare

Workers, custom domains, DNS, Access, tunnels, Hyperdrive, queues, and logs, discovered through the Cloudflare API and Wrangler. Physical Worker names are generated and may change (AGENTS.md).

Rules of engagement

  • Never print secrets: .env files, tunnel tokens, service tokens, database passwords, signer keys, Worker secrets (AGENTS.md).
  • Inspect read-only first: repository, then host, then Cloudflare. Start with the provisioned Axiom view or APL queries instead of tailing the Worker (docs/operations/relay-observability.md:50-53).
  • Do not deploy, migrate, rotate credentials, edit DNS or Access, or restart services unless the request is explicit (AGENTS.md).
  • Production truth lives across three layers. Any single layer, including this page, is a claim.

Go deeper

Primary sources for this page. Read them in the repository, not in a summary, before changing any of this behavior.

Relay plane

  • infra/relay/README.md
  • infra/relay/alchemy.run.ts
  • infra/relay/src/worker.ts
  • infra/relay/src/db.ts
  • infra/relay/src/http/Api.ts
  • infra/relay/src/auth/RelayTokens.ts

Environments and docs

  • infra/relay/src/environments/EnvironmentLinker.ts
  • infra/relay/src/environments/EnvironmentCredentials.ts
  • infra/relay/src/environments/EnvironmentConnector.ts
  • infra/relay/src/environments/ManagedEndpointProvider.ts
  • docs/internals/environment-auth.md
  • docs/operations/relay-observability.md

Continue the atlas

The Signer & authority page covers the local, deterministic key handling on the environment side of this boundary. Contracts holds the wire shapes the relay and its clients share, and Clients shows how web and desktop consume endpoints and tickets.