T3 Connect: OAuth, DPoP, relay, and tunnel
T3 Connect uses a Clerk account credential and a device-held DPoP key to authorize relay control-plane operations, links a local environment through a signed proof, provisions a Cloudflare tunnel, then gives the client a direct DPoP-bound connection to that environment. The relay authorizes, provisions, and brokers setup; normal T3 traffic does not transit it.
What this chapter resolves
- Trace Clerk OAuth from the hosted handoff page through PKCE callback and stored CLI credential.
- Separate a device DPoP key, relay DPoP access token, environment bootstrap credential, environment access token, and WebSocket ticket.
- Explain environment link registration and managed Cloudflare tunnel provisioning without turning the relay into a fictional data proxy.
- Identify the control-plane and launch calls that involve the relay versus the direct environment data plane.
T3 Connect is a connection-launch system, not a hosted copy of an environment. It uses a cloud account to discover and authorize an environment, arranges a managed endpoint when requested, and gives a client credentials to enter that environment. Once connected, the selected environment—not the relay—continues to own T3 HTTP, WebSocket, orchestration, terminal, and project traffic.
1. Clerk establishes account authority; the CLI uses hosted OAuth handoff
Web, desktop, and mobile use the configured Clerk application for cloud account
state. For the headless CLI, the OAuth public client uses PKCE: the process creates
a verifier, challenge, and state, starts either a loopback callback listener or an
out-of-band prompt, and exchanges the resulting authorization code at Clerk’s token
endpoint. The CLI stores the returned OAuth credential in its environment secret
store; t3 connect login does not enable remote exposure.
The browser starts at the hosted /connect page, not Clerk’s authorize endpoint
directly. The request carries state and PKCE challenge in the fragment. After the
hosted page has a Clerk session, it forwards the authorization request with those
parameters intact. This avoids a signed-out Clerk redirect losing the authorize
request. A loopback request supplies a port and returns to
http://127.0.0.1:<port>/callback; an SSH or --headless flow instead uses hosted
/connect/callback, displays code.state, and the terminal checks the state before
exchanging the code.
/**
* Requested at authorize time by the hosted page and honored by the CLI's
* token exchange; keep both sides on this single definition.
*/
export const CONNECT_OAUTH_SCOPES = ["openid", "profile", "email"] as const;
export interface ConnectAuthorizeRequest {
readonly state: string;
readonly challenge: string;
/**
* Present when a loopback CLI initiated the request: the hosted /connect
* page then asks Clerk to redirect the authorization code straight to
* `http://127.0.0.1:<port>/callback` instead of the hosted callback page.
*/
readonly loopbackPort?: number;
}
/**
* The URL the CLI prints for the user to open in a browser. `state` and
* `code_challenge` ride the fragment so they never reach the hosted app's
* server or CDN logs; neither is a secret.
*
* Both CLI flows route through the hosted /connect page rather than hitting
* Clerk's /oauth/authorize directly: a signed-out browser sent straight to
* /oauth/authorize goes through Clerk's sign-in redirect, which does not
* reliably preserve the authorize query parameters (state, response_type,
* code_challenge). The hosted page waits for a Clerk session first, then
* forwards the request with the parameters intact.
*/
export function buildConnectAuthorizeRequestUrl(input: {
readonly hostedAppUrl: string;
readonly state: string;
readonly challenge: string;
readonly loopbackPort?: number;
}): string {
const url = new URL(CONNECT_AUTHORIZE_PATH, input.hostedAppUrl);
url.hash = new URLSearchParams([
[CONNECT_AUTH_STATE_PARAM, input.state],
[CONNECT_AUTH_CHALLENGE_PARAM, input.challenge],
...(input.loopbackPort === undefined
? []
: [[CONNECT_AUTH_PORT_PARAM, String(input.loopbackPort)] as [string, string]]),
]).toString();
return url.toString();
}
export function readConnectAuthorizeRequest(url: URL): ConnectAuthorizeRequest | null {
const params = readHashParams(url);
const state = params.get(CONNECT_AUTH_STATE_PARAM)?.trim() ?? "";
const challenge = params.get(CONNECT_AUTH_CHALLENGE_PARAM)?.trim() ?? "";
if (!state || !challenge) {
return null;
}
const port = params.get(CONNECT_AUTH_PORT_PARAM);
if (port === null) {
return { state, challenge };
}
// A present-but-invalid port means the link was corrupted; reject the whole
// request rather than silently downgrading a loopback flow to the
// out-of-band one, which would strand the waiting CLI.
const loopbackPort = parseLoopbackPort(port.trim());
if (loopbackPort === null) {
return null;
}
return { state, challenge, loopbackPort };
}
function parseLoopbackPort(value: string): number | null {
if (!/^\d{1,5}$/.test(value)) {
return null;
}
const port = Number(value);
return port >= 1 && port <= 65535 ? port : null;
}
/**
* Redirect URI for the CLI's local callback listener. Must stay in sync with
* the redirect URI registered on the Clerk CLI OAuth application.
*/
export function connectLoopbackRedirectUri(port: number): string {
return `http://127.0.0.1:${port}${CONNECT_LOOPBACK_CALLBACK_PATH}`;
}
export function connectCallbackUrl(hostedAppUrl: string): string {
return new URL(CONNECT_CALLBACK_PATH, hostedAppUrl).toString();
}const makePkceRequest = Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const verifier = Encoding.encodeBase64Url(yield* crypto.randomBytes(32));
const challenge = Encoding.encodeBase64Url(
yield* crypto.digest("SHA-256", new TextEncoder().encode(verifier)),
);
const state = Encoding.encodeBase64Url(yield* crypto.randomBytes(16));
return { verifier, challenge, state };
});
export interface OutOfBandOAuthPromptInput {
readonly authorizeUrl: string;
readonly validate: (value: string) => Effect.Effect<string, string>;
}
/**
* Out-of-band OAuth for machines without a local browser (SSH). The user
* opens the hosted /connect URL elsewhere, signs in, and enters the displayed
* code in this terminal. The PKCE verifier never leaves this process, so the
* authorization code is useless to an observer, and the state bundled into
* the blob preserves the loopback flow's CSRF check.
*/
export const outOfBandOAuthLogin = Effect.fn("cloud.cli_token.out_of_band_oauth_login")(function* <
E,
R,
>(promptForCode: (input: OutOfBandOAuthPromptInput) => Effect.Effect<string, E, R>) {
const metadata = yield* cloudCliOAuthConfig;
const hostedAppUrl = yield* hostedAppUrlConfig;
const { verifier, challenge, state } = yield* makePkceRequest;
const authorizationCode = yield* promptForCode({
authorizeUrl: buildConnectAuthorizeRequestUrl({ hostedAppUrl, state, challenge }),
validate: (value) => {
const checked = checkConnectAuthCode(value, state);
return typeof checked === "string" ? Effect.fail(checked) : Effect.succeed(value);
},
}).pipe(
// Clerk authorization codes expire on this horizon anyway; matching the
// loopback flow's timeout turns an abandoned prompt into a clear error.
Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT),
Effect.catchTag("TimeoutError", (cause) =>
Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })),
),
);
// promptForCode is caller-supplied, so re-check the returned value rather
// than trusting that the prompt ran validate.
const authCode = checkConnectAuthCode(authorizationCode, state);
if (typeof authCode === "string") {
return yield* new CloudCliAuthorizationError({ cause: authCode });
}
return yield* exchangeToken(metadata, {
grant_type: "authorization_code",
code: authCode.code,
redirect_uri: connectCallbackUrl(hostedAppUrl),
client_id: metadata.clientId,
code_verifier: verifier,
});2. The device key binds proof, rather than becoming an account password
For managed relay access, a browser creates a P-256 signing key and retains it in
its DPoP key store. Its public JWK yields a thumbprint. Each proof is a signed JWT
with method (htm), normalized target URL (htu), fresh id (jti), issue time, and,
when an access token is being presented, its hash (ath). The private key does not
travel to the relay or environment.
The verifier checks the signature, requested method and URL, expected thumbprint,
optional access-token hash, and a bounded issue-time window. The environment also
persists a thumbprint-plus-jti replay marker. A captured access token therefore is
not sufficient for a DPoP route without a matching fresh proof; a reused proof is
rejected.
const DPOP_DATABASE_NAME = "t3code:cloud-auth";
const DPOP_DATABASE_VERSION = 1;
const DPOP_KEY_STORE_NAME = "keys";
const DPOP_KEY_ID = "relay-dpop-proof-key";
const decodeDpopPublicJwk = Schema.decodeUnknownEffect(DpopPublicJwk);
export const browserCryptoLayer = Layer.succeed(
Crypto.Crypto,
Crypto.make({
randomBytes: (size) => globalThis.crypto.getRandomValues(new Uint8Array(size)),
digest: (algorithm, data) =>
Effect.promise(async () => {
const input = new Uint8Array(data.length);
input.set(data);
return new Uint8Array(await globalThis.crypto.subtle.digest(algorithm, input.buffer));
}),
}),
);
function dpopError(message: string, cause?: unknown) {
return new BrowserDpopError({ message, ...(cause === undefined ? {} : { cause }) });
}
function openDpopDatabase(): Effect.Effect<IDBDatabase, BrowserDpopError> {
return Effect.callback<IDBDatabase, BrowserDpopError>((resume) => {
const request = indexedDB.open(DPOP_DATABASE_NAME, DPOP_DATABASE_VERSION);
request.addEventListener("error", () =>
resume(
Effect.fail(dpopError("Could not open DPoP key storage.", request.error ?? undefined)),
),
);
request.addEventListener("upgradeneeded", () => {
if (!request.result.objectStoreNames.contains(DPOP_KEY_STORE_NAME)) {
request.result.createObjectStore(DPOP_KEY_STORE_NAME);
}
});
request.addEventListener("success", () => resume(Effect.succeed(request.result)));
});
}
export function readStoredBrowserDpopKey(): Effect.Effect<BrowserDpopKey | null, BrowserDpopError> {
if (typeof indexedDB === "undefined") {
return Effect.succeed(null);
}
return Effect.acquireUseRelease(
openDpopDatabase(),
(database) =>
Effect.callback<BrowserDpopKey | null, BrowserDpopError>((resume) => {
const request = database
.transaction(DPOP_KEY_STORE_NAME, "readonly")
.objectStore(DPOP_KEY_STORE_NAME)
.get(DPOP_KEY_ID);
request.addEventListener("error", () =>
resume(Effect.fail(dpopError("Could not read DPoP key.", request.error ?? undefined))),
);
request.addEventListener("success", () =>
resume(Effect.succeed((request.result as BrowserDpopKey | undefined) ?? null)),
);
}),
(database) => Effect.sync(() => database.close()),
);
}
export function writeStoredBrowserDpopKey(
key: BrowserDpopKey,
): Effect.Effect<void, BrowserDpopError> {
if (typeof indexedDB === "undefined") {
return Effect.void;
}
return Effect.acquireUseRelease(
openDpopDatabase(),
(database) =>
Effect.callback<void, BrowserDpopError>((resume) => {
const transaction = database.transaction(DPOP_KEY_STORE_NAME, "readwrite");
transaction.addEventListener("error", () =>
resume(
Effect.fail(dpopError("Could not write DPoP key.", transaction.error ?? undefined)),
),
);
transaction.addEventListener("complete", () => resume(Effect.void));
transaction.objectStore(DPOP_KEY_STORE_NAME).put(key, DPOP_KEY_ID);
}),
(database) => Effect.sync(() => database.close()),
);
}
export const generateBrowserDpopKey = Effect.gen(function* () {
const generated = yield* Effect.tryPromise({
try: () =>
crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
"sign",
"verify",
]) as Promise<CryptoKeyPair>,
catch: (cause) => dpopError("Could not generate DPoP proof key.", cause),
});
const privateJwk = yield* Effect.tryPromise({
try: () => crypto.subtle.exportKey("jwk", generated.privateKey),
catch: (cause) => dpopError("Could not export DPoP private key.", cause),
});
const publicJwk = yield* Effect.tryPromise({
try: () => crypto.subtle.exportKey("jwk", generated.publicKey),
catch: (cause) => dpopError("Could not export DPoP public key.", cause),
}).pipe(
Effect.flatMap((jwk) => decodeDpopPublicJwk(jwk)),
Effect.mapError((cause) =>
cause instanceof BrowserDpopError
? cause
: dpopError("Generated DPoP public key is invalid.", cause),
),
);
const privateKey = yield* Effect.tryPromise({
try: () => importJWK(privateJwk as JWK, "ES256", { extractable: false }) as Promise<CryptoKey>,
catch: (cause) => dpopError("Could not import DPoP private key.", cause),
});
return {
privateKey,
publicJwk,
thumbprint: computeDpopJwkThumbprint(publicJwk),
};
});import { p256 } from "@noble/curves/nist";
import { sha256 } from "@noble/hashes/sha2";
import * as Encoding from "effect/Encoding";
import * as Option from "effect/Option";
import * as Result from "effect/Result";
import * as Schema from "effect/Schema";
import { DpopPublicJwk as DpopPublicJwkSchema, normalizeDpopHtu } from "./dpopCommon.ts";
import type { DpopPublicJwk as DpopPublicJwkType } from "./dpopCommon.ts";
import { stableStringify } from "./relaySigning.ts";
const DPOP_TYP = "dpop+jwt";
const DPOP_ALG = "ES256";
const DEFAULT_MAX_AGE_SECONDS = 300;
export const DpopPublicJwk = DpopPublicJwkSchema;
export type DpopPublicJwk = DpopPublicJwkType;
export { normalizeDpopHtu };
const DpopJwtHeaderPublicJwk = Schema.Struct({
...DpopPublicJwkSchema.fields,
d: Schema.optionalKey(Schema.Never),
});
const DpopJwtHeaderJson = Schema.fromJsonString(
Schema.Struct({
typ: Schema.Literal(DPOP_TYP),
alg: Schema.Literal(DPOP_ALG),
jwk: DpopJwtHeaderPublicJwk,
}),
);
const decodeDpopJwtHeaderJson = Schema.decodeUnknownOption(DpopJwtHeaderJson);
const DpopJwtPayloadJson = Schema.fromJsonString(
Schema.Struct({
htm: Schema.String.check(Schema.isNonEmpty()),
htu: Schema.String.check(Schema.isNonEmpty()),
jti: Schema.String.check(Schema.isNonEmpty()),
iat: Schema.Int,
ath: Schema.optionalKey(Schema.String),
}),
);
const decodeDpopJwtPayloadJson = Schema.decodeUnknownOption(DpopJwtPayloadJson);
export type DpopVerificationResult =
| {
readonly ok: true;
readonly thumbprint: string;
readonly jti: string;
readonly iat: number;
}
| {
readonly ok: false;
readonly reason: string;
};
function base64UrlToBytes(value: string): Uint8Array {
return Result.getOrThrow(Encoding.decodeBase64Url(value));
}
function decodeBase64UrlDpopJwtHeader(value: string) {
return decodeDpopJwtHeaderJson(Result.getOrThrow(Encoding.decodeBase64UrlString(value)));
}
function decodeBase64UrlDpopJwtPayload(value: string) {
return decodeDpopJwtPayloadJson(Result.getOrThrow(Encoding.decodeBase64UrlString(value)));
}
function dpopThumbprintInput(jwk: DpopPublicJwkType): string {
return stableStringify({
crv: jwk.crv,
kty: jwk.kty,
x: jwk.x,
y: jwk.y,
});
}
export function computeDpopJwkThumbprint(jwk: DpopPublicJwkType): string {
return Encoding.encodeBase64Url(sha256(new TextEncoder().encode(dpopThumbprintInput(jwk))));
}
export function computeDpopAccessTokenHash(accessToken: string): string {
return Encoding.encodeBase64Url(sha256(new TextEncoder().encode(accessToken)));
}
function publicKeyBytesFromJwk(jwk: DpopPublicJwkType): Uint8Array {
const x = base64UrlToBytes(jwk.x);
const y = base64UrlToBytes(jwk.y);
if (x.length !== 32 || y.length !== 32) {
throw new Error("Invalid P-256 public key coordinate length.");
}
const publicKey = new Uint8Array(65);
publicKey[0] = 0x04;
publicKey.set(x, 1);
publicKey.set(y, 33);
return publicKey;
}
export function verifyDpopProof(input: {
readonly proof: string | null | undefined;
readonly method: string;
readonly url: string;
readonly nowEpochSeconds: number;
readonly expectedThumbprint?: string;
readonly expectedAccessToken?: string;
readonly maxAgeSeconds?: number;
}): DpopVerificationResult {
if (!input.proof?.trim()) {
return { ok: false, reason: "Missing DPoP proof." };
}
const parts = input.proof.split(".");
if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {
return { ok: false, reason: "Invalid DPoP compact JWT." };
}
try {
const header = decodeBase64UrlDpopJwtHeader(parts[0]);
const payload = decodeBase64UrlDpopJwtPayload(parts[1]);
if (Option.isNone(header)) {
return { ok: false, reason: "Invalid DPoP JWT header." };
}
if (Option.isNone(payload)) {
return { ok: false, reason: "Invalid DPoP JWT payload." };
}
const thumbprint = computeDpopJwkThumbprint(header.value.jwk);
if (input.expectedThumbprint && thumbprint !== input.expectedThumbprint) {
return { ok: false, reason: "DPoP key thumbprint mismatch." };
}
if (payload.value.htm.toUpperCase() !== input.method.toUpperCase()) {
return { ok: false, reason: "DPoP method mismatch." };
}
const normalizedHtu = normalizeDpopHtu(input.url);
if (normalizedHtu === null || payload.value.htu !== normalizedHtu) {
return { ok: false, reason: "DPoP URL mismatch." };
}
if (input.expectedAccessToken) {
const expectedAth = computeDpopAccessTokenHash(input.expectedAccessToken);
if (payload.value.ath !== expectedAth) {
return { ok: false, reason: "DPoP access token hash mismatch." };
}
}
const maxAgeSeconds = input.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
if (
payload.value.iat > input.nowEpochSeconds + 5 ||
input.nowEpochSeconds - payload.value.iat > maxAgeSeconds
) {
return { ok: false, reason: "DPoP proof is outside the allowed time window." };
}
const signature = base64UrlToBytes(parts[2]);
const signatureInputHash = sha256(new TextEncoder().encode(`${parts[0]}.${parts[1]}`));
const verified = p256.verify(
signature,
signatureInputHash,
publicKeyBytesFromJwk(header.value.jwk),
{
prehash: false,
format: "compact",
},
);
return verified
? {
ok: true,
thumbprint,
jti: payload.value.jti,
iat: payload.value.iat,
}
: { ok: false, reason: "Invalid DPoP signature." };
} catch {
return { ok: false, reason: "Invalid DPoP proof." };
}
}3. Linking registers an environment and its managed endpoint intent
Linking has two authorities. First the signed-in client asks the relay for an environment-link challenge. It then asks the local primary environment to sign a link proof containing its descriptor, environment public key, requested endpoint, origin, scopes, and challenge. The relay verifies the proof and challenge, consumes replay nonces, checks that the descriptor and environment id agree, and upserts the account-to-environment link.
For a managed link, the relay accepts only a loopback origin, provisions a managed
endpoint, then returns both a per-environment relay credential and runtime connector
configuration. The local environment persists that configuration and starts the
managed connector. --publish-only deliberately takes a different path: it links
activity publishing but asks for no managed tunnel, so a client must reach the
environment out of band.
export function linkPrimaryEnvironmentToCloud(input: {
readonly target: CloudLinkTarget;
readonly clerkToken: string;
readonly mode?: CloudLinkMode;
}): Effect.Effect<
void,
CloudEnvironmentLinkError,
EnvironmentRegistry | HttpClient.HttpClient | ManagedRelay.ManagedRelayClient
> {
return Effect.gen(function* () {
const configuredRelayUrl = relayUrl();
if (!configuredRelayUrl) {
return yield* new CloudEnvironmentLinkError({
message: "T3CODE_RELAY_URL is not configured.",
});
}
const managedTunnelsEnabled = (input.mode ?? "managed") === "managed";
const providerKind = managedTunnelsEnabled
? MANAGED_ENDPOINT_PROVIDER_KIND
: PUBLISH_ONLY_PROVIDER_KIND;
const relayClient = yield* ManagedRelay.ManagedRelayClient;
const environmentClient = yield* makeEnvironmentHttpApiClient(input.target.httpBaseUrl);
if (managedTunnelsEnabled) {
yield* ensureRelayClientAvailable(EnvironmentId.make(input.target.environmentId));
}
const challenge = yield* relayClient
.createEnvironmentLinkChallenge({
clerkToken: input.clerkToken,
payload: {
notificationsEnabled: true,
liveActivitiesEnabled: true,
managedTunnelsEnabled,
},
})
.pipe(
Effect.mapError(
decodedRelayClientError(
`${configuredRelayUrl}/v1/client/environment-link-challenges failed`,
),
),
);
const proof = yield* environmentClient.connect
.linkProof({
headers: {},
payload: {
challenge: challenge.challenge,
relayIssuer: configuredRelayUrl,
endpoint: {
httpBaseUrl: input.target.httpBaseUrl,
wsBaseUrl: input.target.wsBaseUrl,
providerKind,
},
origin: endpointOrigin(input.target.httpBaseUrl),
},
})
.pipe(Effect.mapError(environmentApiError("Could not obtain environment link proof.")));
const link = yield* relayClient
.linkEnvironment({
clerkToken: input.clerkToken,
payload: {
proof,
notificationsEnabled: true,
liveActivitiesEnabled: true,
managedTunnelsEnabled,
},
})
.pipe(
Effect.mapError(
decodedRelayClientError(`${configuredRelayUrl}/v1/client/environment-links failed`),
),
);
yield* ensureLinkedEnvironmentMatches({
expectedEnvironmentId: input.target.environmentId,
expectedProviderKind: providerKind,
link,
});
yield* environmentClient.connect
.relayConfig({
headers: {},
payload: {
relayUrl: configuredRelayUrl,
relayIssuer: link.relayIssuer,
cloudUserId: link.cloudUserId,
environmentCredential: link.environmentCredential,
cloudMintPublicKey: link.cloudMintPublicKey,
endpointRuntime: link.endpointRuntime,
},
})
.pipe(Effect.mapError(environmentApiError("Could not configure environment relay access.")));
}).pipe(Effect.provide(primaryEnvironmentHttpLayer)); const challenge = yield* relayTokens.verifyLinkChallenge({
token: verified.challenge,
userId: input.userId,
request: {
notificationsEnabled: input.request.notificationsEnabled,
liveActivitiesEnabled: input.request.liveActivitiesEnabled,
managedTunnelsEnabled: input.request.managedTunnelsEnabled,
},
nowEpochSeconds: nowSeconds,
});
if (challenge === null) {
return yield* new EnvironmentLinkProofInvalid({
userId: input.userId,
environmentId: verified.environmentId,
reason: "challenge_invalid",
stage: "verify_challenge",
});
}
const expiresAt = DateTime.make(verified.exp * 1_000);
if (expiresAt._tag === "None") {
return yield* new EnvironmentLinkProofInvalid({
userId: input.userId,
environmentId: verified.environmentId,
reason: "invalid_signature_or_scope",
stage: "validate_expiration",
});
}
const consumedNonce = yield* proofReplay.consume({
thumbprint: verified.environmentPublicKey,
jti: verified.jti,
iat: verified.iat,
expiresAt: expiresAt.value,
});
if (!consumedNonce) {
return yield* new EnvironmentLinkProofInvalid({
userId: input.userId,
environmentId: verified.environmentId,
reason: "replayed_nonce",
stage: "consume_proof_nonce",
});
}
const consumedChallenge = yield* proofReplay.consume({
thumbprint: "relay-environment-link-challenge",
jti: challenge.jti,
iat: challenge.iat,
expiresAt: expiresAt.value,
});
if (!consumedChallenge) {
return yield* new EnvironmentLinkProofInvalid({
userId: input.userId,
environmentId: verified.environmentId,
reason: "challenge_invalid",
stage: "consume_challenge_nonce",
});
}
if (input.request.managedTunnelsEnabled && !isLoopbackManagedTunnelOrigin(verified.origin)) {
return yield* new EnvironmentLinkProofInvalid({
userId: input.userId,
environmentId: verified.environmentId,
reason: "origin_not_allowed",
stage: "validate_origin",
});
}
// Downgrading a managed link to publish-only must release the tunnel and
// DNS that were provisioned for it — nothing else cleans them up until a
// full unlink. Best effort: a cleanup failure must not block the link
// itself, and the provider treats an absent allocation as already
// deprovisioned, so retrying on every non-tunnel link is cheap.
if (!input.request.managedTunnelsEnabled) {
yield* managedEndpointProvider
.deprovision({
userId: input.userId,
environmentId: verified.environmentId,
})
.pipe(
Effect.tapError((error) =>
Effect.logWarning("managed endpoint deprovision on publish-only link failed", {
environmentId: verified.environmentId,
errorTag: error._tag,
}),
),
Effect.ignore,
);
}
const provisioned = input.request.managedTunnelsEnabled
? yield* managedEndpointProvider.provision({
userId: input.userId,
environmentId: verified.environmentId,
origin: verified.origin,
})
: null;
const endpoint = provisioned?.endpoint ?? verified.endpoint;
// The secure-endpoint requirement only matters when the relay advertises
// this endpoint for other devices to reach (managed tunnel). Publish-only
// links are reached out of band (e.g. Tailscale) and their stored endpoint
// is never used for routing, so a nominal endpoint is acceptable.
if (input.request.managedTunnelsEnabled && !isSecureManagedEndpoint(endpoint)) {
return yield* new EnvironmentLinkProofInvalid({
userId: input.userId,
environmentId: verified.environmentId,
reason: "endpoint_not_secure",
stage: "validate_endpoint",
});
}
yield* links.upsert({ ...input, proof: verified, endpoint });
const environmentCredential = yield* credentials.create({
environmentId: verified.environmentId,
environmentPublicKey: verified.environmentPublicKey,
});
return {
environmentId: verified.environmentId,
endpoint,
endpointRuntime: provisioned?.runtime ?? null,
environmentCredential,
};4. A tunnel exposes the environment; it does not route through the relay
Provisioning derives a stable hostname and tunnel name, reserves an allocation,
creates or reuses the Cloudflare tunnel, points its ingress at the local loopback
HTTP origin, creates a proxied CNAME to *.cfargotunnel.com, obtains a connector
token, and marks the allocation ready. The environment runtime launches the managed
client as tunnel run with that token and supervises it.
That is an endpoint-exposure path: client traffic can travel through the managed Cloudflare tunnel to the environment. It is not a relay data hop. The relay’s own README says that normal API and WebSocket traffic goes directly between client and selected environment after connection; the client runtime independently constructs the environment’s token and WebSocket-ticket requests from the endpoint URL.
Zoom with the controls, +/−, or Ctrl/⌘ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.
Text equivalent
Control-plane paths are client to Clerk, client to relay, and relay to environment while linking or minting. The environment’s cloudflared connector reaches the Cloudflare tunnel service. After launch, client-to-environment HTTP and WebSocket traffic uses the managed endpoint directly. The relay is not on that normal traffic path.
infra/relay/README.md:6–29 ↗apps/web/src/cloud/linkEnvironment.ts:402–493 ↗infra/relay/src/environments/ManagedEndpointProvider.ts:604–855 ↗apps/server/src/cloud/ManagedEndpointRuntime.ts:187–287 ↗infra/relay/src/environments/EnvironmentConnector.ts:541–670 ↗packages/client-runtime/src/authorization/service.ts:180–299 ↗ provision: Effect.fn("relay.managed_endpoint_provider.provision")(function* (input) {
yield* Effect.annotateCurrentSpan({
"relay.user_id": input.userId,
"relay.environment_id": input.environmentId,
"relay.managed_endpoint.origin_host": input.origin.localHttpHost,
"relay.managed_endpoint.origin_port": input.origin.localHttpPort,
});
if (!isLoopbackOrigin(input.origin)) {
return yield* new ManagedEndpointOriginNotAllowed({
userId: input.userId,
environmentId: input.environmentId,
host: input.origin.localHttpHost,
port: input.origin.localHttpPort,
});
}
const cf = yield* requireCloudflareSettings(config, input);
const environmentHash = yield* crypto
.digest(
"SHA-256",
new TextEncoder().encode(
managedEndpointDigestInput(cf.namespace, input.userId, input.environmentId),
),
)
.pipe(
Effect.map(Encoding.encodeHex),
Effect.mapError(
(cause) =>
new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "derive-environment-hash",
cause,
}),
),
);
const requestedHostname = managedEndpointHostname(
cf.namespace,
cf.baseDomain,
environmentHash,
);
const requestedTunnelName = managedEndpointTunnelName(cf.namespace, environmentHash);
yield* tunnelLimits
.ensureCapacity({
userId: input.userId,
environmentId: input.environmentId,
})
.pipe(
Effect.catchTags({
ManagedTunnelLimitPersistenceError: (cause) =>
Effect.fail(
new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "check-tunnel-limit",
hostname: requestedHostname,
tunnelName: requestedTunnelName,
cause,
}),
),
}),
);
const allocation = yield* allocations
.reserve({
userId: input.userId,
environmentId: input.environmentId,
hostname: requestedHostname,
tunnelName: requestedTunnelName,
})
.pipe(
Effect.mapError(
(cause) =>
new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "reserve-allocation",
hostname: requestedHostname,
tunnelName: requestedTunnelName,
cause,
}),
),
);
const { hostname, tunnelName } = allocation;
const tunnelResponse = yield* tunnels.list({ name: tunnelName, isDeleted: false }).pipe(
Effect.map((tunnels) => tunnels.result),
Effect.map(Arr.findFirst((tunnel) => tunnel.name === tunnelName)),
Effect.flatMap(
Option.match({
onSome: (tunnel) => Effect.succeed(tunnel),
onNone: () => tunnels.create({ name: tunnelName, configSrc: "cloudflare" }),
}),
),
Effect.mapError(
(cause) =>
new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "ensure-tunnel",
hostname,
tunnelName,
cause,
}),
),
);
if (!tunnelResponse.id || tunnelResponse.name !== tunnelName) {
return yield* new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "validate-tunnel-response",
hostname,
tunnelName,
...(tunnelResponse.id ? { returnedTunnelId: tunnelResponse.id } : {}),
...(tunnelResponse.name ? { returnedTunnelName: tunnelResponse.name } : {}),
});
}
const tunnel = { id: tunnelResponse.id, name: tunnelResponse.name };
yield* allocations
.recordTunnel({
userId: input.userId,
environmentId: input.environmentId,
tunnelId: tunnel.id,
})
.pipe(
Effect.mapError(
(cause) =>
new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "record-tunnel",
hostname,
tunnelName,
tunnelId: tunnel.id,
cause,
}),
),
);
yield* tunnels
.putConfiguration(tunnel.id, {
ingress: [
{
hostname,
service: formatOriginService(input.origin),
},
{ service: "http_status:404" },
],
})
.pipe(
Effect.mapError(
(cause) =>
new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "configure-tunnel",
hostname,
tunnelName,
tunnelId: tunnel.id,
cause,
}),
),
);
const dnsRecord = {
type: "CNAME",
name: hostname,
content: `${tunnel.id}.cfargotunnel.com`,
ttl: 1,
proxied: true,
} as const;
const dnsRecordId = yield* ensureDnsRecord(hostname, allocation.dnsRecordId, dnsRecord).pipe(
Effect.mapError(
(cause) =>
new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "ensure-dns-record",
hostname,
tunnelName,
tunnelId: tunnel.id,
...(allocation.dnsRecordId === null ? {} : { dnsRecordId: allocation.dnsRecordId }),
cause,
}),
),
);
yield* allocations
.recordDns({
userId: input.userId,
environmentId: input.environmentId,
dnsRecordId,
})
.pipe(
Effect.mapError(
(cause) =>
new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "record-dns",
hostname,
tunnelName,
tunnelId: tunnel.id,
dnsRecordId,
cause,
}),
),
);
const connectorToken = yield* tunnels.getToken(tunnel.id).pipe(
Effect.mapError(
(cause) =>
new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "get-tunnel-token",
hostname,
tunnelName,
tunnelId: tunnel.id,
dnsRecordId,
cause,
}),
),
);
yield* allocations
.markReady({
userId: input.userId,
environmentId: input.environmentId,
})
.pipe(
Effect.mapError(
(cause) =>
new ManagedEndpointProvisioningFailed({
userId: input.userId,
environmentId: input.environmentId,
stage: "mark-allocation-ready",
hostname,
tunnelName,
tunnelId: tunnel.id,
dnsRecordId,
cause,
}),
),
);
return {
endpoint: managedEndpointForHostname(hostname),
runtime: {
providerKind: "cloudflare_tunnel",
connectorToken,
tunnelId: tunnel.id,
tunnelName: tunnel.name,
},
} satisfies ManagedEndpointProvisioningResult; reconcileConfig = Effect.fn("CloudManagedEndpointRuntime.reconcileConfig")(function* (config) {
if (!config || config.providerKind !== "cloudflare_tunnel") {
yield* stopActive;
return config
? { status: "unsupported", providerKind: config.providerKind }
: { status: "disabled" };
}
const nextConfigKey = runtimeConfigKey(config);
const active = yield* Ref.get(activeRef);
if (active?.configKey === nextConfigKey) {
const isRunning = yield* active.child.isRunning.pipe(Effect.orElseSucceed(() => false));
if (isRunning) {
return {
status: "running",
providerKind: "cloudflare_tunnel",
pid: Number(active.child.pid),
...(active.config.tunnelId ? { tunnelId: active.config.tunnelId } : {}),
...(active.config.tunnelName ? { tunnelName: active.config.tunnelName } : {}),
} satisfies CloudManagedEndpointRuntimeStatus;
}
}
yield* stopActive;
const executable = yield* relayClient.resolve;
if (executable.status !== "available") {
return {
status: "failed",
providerKind: "cloudflare_tunnel",
reason:
executable.status === "unsupported"
? `Relay client is unsupported on ${executable.platform}-${executable.arch}.`
: "The relay client is not installed.",
...(config.tunnelId ? { tunnelId: config.tunnelId } : {}),
...(config.tunnelName ? { tunnelName: config.tunnelName } : {}),
} satisfies CloudManagedEndpointRuntimeStatus;
}
const connectorScope = yield* Scope.make("sequential");
const child = yield* spawner
.spawn(
ChildProcess.make(executable.executablePath, ["tunnel", "run"], {
detached: false,
env: {
...process.env,
TUNNEL_TOKEN: config.connectorToken,
},
shell: false,
stderr: "pipe",
stdout: "pipe",
}),
)
.pipe(
Effect.provideService(Scope.Scope, connectorScope),
Effect.tap((child) =>
Effect.logInfo("Relay client process started; waiting for tunnel connection", {
pid: Number(child.pid),
tunnelId: config.tunnelId,
tunnelName: config.tunnelName,
}),
),
Effect.catch((cause) =>
Effect.logWarning("Failed to start relay client", {
cause,
tunnelId: config.tunnelId,
tunnelName: config.tunnelName,
}).pipe(
Effect.andThen(Scope.close(connectorScope, Exit.void).pipe(Effect.ignore)),
Effect.as({
status: "failed",
providerKind: "cloudflare_tunnel",
reason: String(cause),
...(config.tunnelId ? { tunnelId: config.tunnelId } : {}),
...(config.tunnelName ? { tunnelName: config.tunnelName } : {}),
} satisfies CloudManagedEndpointRuntimeStatus),
),
),
);
if ("status" in child && child.status === "failed") {
return child;
}
if (!("status" in child)) {
const connector = {
child,
scope: connectorScope,
configKey: nextConfigKey,
config,
} satisfies ActiveConnector;
yield* Ref.set(activeRef, connector);
yield* Effect.forkIn(observeConnectorOutput(connector), connectorScope);
yield* Effect.forkIn(superviseConnector(connector), connectorScope);
return {
status: "running",
providerKind: "cloudflare_tunnel",
pid: Number(child.pid),
...(config.tunnelId ? { tunnelId: config.tunnelId } : {}),
...(config.tunnelName ? { tunnelName: config.tunnelName } : {}),
} satisfies CloudManagedEndpointRuntimeStatus;5. The exact credential ladder crosses the relay once, then leaves it
The verbs matter more than calling every string a “token.” A fresh connection takes this ladder:
- A Clerk session credential authorizes the account with the relay.
- The client’s DPoP key signs a proof; the relay exchanges the Clerk credential for a scoped, short-lived relay DPoP access token and requires proof of the same key.
- The relay’s
connectoperation creates a two-minute signed mint request bound to the client key thumbprint and sends it to the managed environment. The verified response supplies an environment bootstrap credential. - The client calls that environment’s
/oauth/tokendirectly with the bootstrap credential and a DPoP proof. The environment issues a DPoP-bound environment access token. - The client calls that environment’s WebSocket-ticket endpoint directly with the environment token and another proof, then attaches the resulting ticket to the environment WebSocket URL.
- The client and environment carry ordinary T3 API/WebSocket work directly. The tunnel is the reachable endpoint; the relay is not the normal hot path.
The bootstrap credential is intentionally not the long-lived data-plane session, and the relay DPoP token is intentionally not an environment token. That separation is why a relay-side account operation, a device proof, an environment access session, and a ticket can have distinct expiry, audience, and replay rules.
connect: Effect.fn("relay.environment_connector.connect")(function* (input) {
yield* Effect.annotateCurrentSpan({
"relay.environment_id": input.environmentId,
"relay.operation": "connect",
"relay.connect.has_device_id": input.deviceId !== undefined,
...(input.deviceId ? { "relay.mobile.device_id": input.deviceId } : {}),
});
if (input.clientProofKeyThumbprint.trim().length === 0) {
return yield* new EnvironmentConnectNotAuthorized({
environmentId: input.environmentId,
operation: "connect",
reason: "client_proof_key_thumbprint_missing",
});
}
const { link, allocation } = yield* Effect.all(
{
link: links.getForUser(input),
allocation: allocations.get(input),
},
{ concurrency: 2 },
);
if (!link) {
return yield* new EnvironmentConnectNotAuthorized({
environmentId: input.environmentId,
operation: "connect",
reason: "environment_link_not_found",
});
}
const endpoint = yield* resolveManagedEndpoint({
operation: "connect",
link,
allocation,
});
const now = yield* DateTime.now;
const expiresAt = DateTime.add(now, { minutes: 2 });
const nonce = yield* crypto.randomUUIDv4.pipe(
Effect.mapError(
(cause) =>
new EnvironmentMintRequestFailed({
environmentId: input.environmentId,
operation: "connect",
cause,
}),
),
);
const payload = {
iss: relayIssuer,
aud: `t3-env:${link.environmentId}`,
sub: input.userId,
jti: yield* crypto.randomUUIDv4.pipe(
Effect.mapError(
(cause) =>
new EnvironmentMintRequestFailed({
environmentId: input.environmentId,
operation: "connect",
cause,
}),
),
),
iat: Math.floor(now.epochMilliseconds / 1_000),
exp: Math.floor(expiresAt.epochMilliseconds / 1_000),
environmentId: link.environmentId,
clientProofKeyThumbprint: input.clientProofKeyThumbprint,
cnf: { jkt: input.clientProofKeyThumbprint },
...(input.deviceId ? { deviceId: input.deviceId } : {}),
nonce,
scope: ["environment:connect"],
} satisfies RelayCloudMintCredentialProofPayload;
const proof = yield* signRelayJwt({
privateKey: Redacted.value(settings.cloudMintPrivateKey),
typ: RELAY_MINT_REQUEST_TYP,
payload,
}).pipe(
Effect.mapError(
(cause) =>
new EnvironmentMintRequestFailed({
environmentId: input.environmentId,
operation: "connect",
cause,
}),
),
);
const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl);
const decoded = yield* environmentClient.connect
.t3MintCredential({ payload: { proof } })
.pipe(
withoutRedirects,
Effect.mapError(
(cause) =>
new EnvironmentMintRequestFailed({
environmentId: input.environmentId,
operation: "connect",
cause,
}),
),
Effect.timeoutOption(Duration.millis(ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS)),
Effect.flatMap(
Option.match({
onNone: () =>
Effect.fail(
new EnvironmentMintRequestTimedOut({
environmentId: input.environmentId,
timeoutMs: ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS,
}),
),
onSome: Effect.succeed,
}),
),
);
const verified = yield* verifyEnvironmentResponse({
response: decoded,
environmentId: input.environmentId,
requestNonce: nonce,
clientProofKeyThumbprint: input.clientProofKeyThumbprint,
environmentPublicKeys: [link.environmentPublicKey],
relayIssuer,
nowEpochSeconds: Math.floor(now.epochMilliseconds / 1_000),
});
if (!verified) {
return yield* new EnvironmentMintResponseInvalid({
environmentId: input.environmentId,
operation: "connect",
});
}
return {
environmentId: link.environmentId,
endpoint,
credential: decoded.credential,
expiresAt: decoded.expiresAt,
}; const authorizeDpop = Effect.fn("clientRuntime.connection.remote.authorizeDpop")(
function* (input: {
readonly expectedEnvironmentId: Parameters<
RemoteEnvironmentAuthorization["Service"]["authorizeDpop"]
>[0]["expectedEnvironmentId"];
readonly obtainBootstrap: Parameters<
RemoteEnvironmentAuthorization["Service"]["authorizeDpop"]
>[0]["obtainBootstrap"];
}) {
const thumbprint = yield* signer.thumbprint.pipe(
Effect.mapError(
() =>
new ConnectionBlockedError({
reason: "configuration",
detail: "Could not load the environment authorization key.",
}),
),
Effect.withSpan("environment.authorization.dpopKey.resolve"),
);
const now = yield* Clock.currentTimeMillis;
const cached = yield* tokenStore
.get(input.expectedEnvironmentId)
.pipe(Effect.withSpan("environment.authorization.accessToken.cache"));
if (
Option.isSome(cached) &&
cached.value.environmentId === input.expectedEnvironmentId &&
cached.value.dpopThumbprint === thumbprint &&
cached.value.expiresAtEpochMs > now + TOKEN_EXPIRY_SAFETY_MARGIN_MS
) {
yield* Effect.annotateCurrentSpan({
"connection.remote_token_cache": "hit",
});
const cachedSocket = yield* createDpopSocketUrl(
cached.value,
CACHED_ENDPOINT_SOCKET_TIMEOUT_MS,
).pipe(Effect.result);
if (Result.isSuccess(cachedSocket)) {
return {
environmentId: cached.value.environmentId,
label: cached.value.label,
httpBaseUrl: cached.value.endpoint.httpBaseUrl,
socketUrl: cachedSocket.success,
httpAuthorization: {
_tag: "Dpop" as const,
accessToken: cached.value.accessToken,
},
};
}
if (cachedSocket.failure._tag === "ConnectionBlockedError") {
return yield* mapDpopSocketError(cachedSocket.failure);
}
yield* tokenStore
.remove(input.expectedEnvironmentId)
.pipe(Effect.withSpan("environment.authorization.accessToken.remove"));
}
yield* Effect.annotateCurrentSpan({
"connection.remote_token_cache": "miss",
});
const bootstrap = yield* input.obtainBootstrap;
const descriptor = yield* fetchDescriptor(bootstrap.endpoint.httpBaseUrl).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.withSpan("environment.authorization.descriptor"),
);
if (descriptor.environmentId !== input.expectedEnvironmentId) {
return yield* environmentMismatchError({
expected: input.expectedEnvironmentId,
actual: descriptor.environmentId,
});
}
const bootstrapProof = yield* signer
.createProof({
method: "POST",
url: environmentEndpointUrl(bootstrap.endpoint.httpBaseUrl, "/oauth/token"),
})
.pipe(
Effect.mapError(
() =>
new ConnectionBlockedError({
reason: "configuration",
detail: "Could not create the environment authorization proof.",
}),
),
);
const access = yield* exchangeRemoteDpopAccessToken({
httpBaseUrl: bootstrap.endpoint.httpBaseUrl,
credential: bootstrap.credential,
dpopProof: bootstrapProof,
scopes: presentation.scopes,
clientMetadata: presentation.metadata,
}).pipe(
Effect.mapError(mapRemoteEnvironmentError),
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.withSpan("environment.authorization.accessToken.exchange"),
);
const issuedAt = yield* Clock.currentTimeMillis;
const token = new TokenStore.RemoteDpopAccessToken({
environmentId: descriptor.environmentId,
label: descriptor.label,
endpoint: bootstrap.endpoint,
accessToken: access.access_token,
expiresAtEpochMs: issuedAt + access.expires_in * 1_000,
dpopThumbprint: thumbprint,
});
const socketUrl = yield* createDpopSocketUrl(token).pipe(Effect.mapError(mapDpopSocketError));
yield* tokenStore
.put(token)
.pipe(Effect.withSpan("environment.authorization.accessToken.persist"));
return {
environmentId: descriptor.environmentId,
label: descriptor.label,
httpBaseUrl: bootstrap.endpoint.httpBaseUrl,
socketUrl,
httpAuthorization: {
_tag: "Dpop" as const,
accessToken: token.accessToken,
},
};
},
);Launch the connection, then move the work on a different path
Choose a manual step. The lab distinguishes the short setup exchange from the HTTP and WebSocket traffic that follows it.
Control plane · account and endpoint authorization
Clerk session reaches the relay
A signed-in client presents its Clerk credential to list or link environments. This authorizes relay control-plane work; it is not an environment WebSocket credential.
- Sender → receiver
- Client → relay
- Proof in use
- Clerk bearer credential
- What becomes possible
- Discovery, link challenge, or relay DPoP token exchange
Step 1 of 6. Control plane.
Static credential ladder and plane boundary
| Step | Path | Credential or proof | Result |
|---|---|---|---|
| 1 | Client → relay | Clerk bearer credential | Account-scoped relay operations |
| 2 | Client → relay | Device P-256 DPoP proof; relay issues DPoP-bound access token | Proof-bound relay request authority |
| 3 | Relay → managed environment | Relay-signed short-lived mint request, bound to the client key thumbprint | Environment bootstrap credential returned through the brokered connection flow |
| 4 | Client → managed environment | Bootstrap credential plus DPoP proof | Environment DPoP access token |
| 5 | Client → managed environment | Environment DPoP access token plus fresh proof | WebSocket ticket |
| 6 | Client ↔ managed environment | Ticketed WebSocket and direct API authorization | Normal T3 HTTP and WebSocket traffic; it does not traverse the relay |
6. Evidence boundary: what this pinned source does and does not establish
The compact model is: Clerk identifies the account; DPoP proves the device; the relay authorizes and launches; the tunnel exposes; the environment admits; the client and environment do the work. Drawing the relay across every subsequent arrow makes the architecture less secure to reason about and less true to the code.