diff --git a/.github/workflows/deploy-relay.yml b/.github/workflows/deploy-relay.yml index bb2c8ff48f05..ce3d5305e601 100644 --- a/.github/workflows/deploy-relay.yml +++ b/.github/workflows/deploy-relay.yml @@ -4,6 +4,7 @@ on: push: branches: - main + workflow_dispatch: permissions: contents: read @@ -17,7 +18,6 @@ concurrency: jobs: deploy_relay: name: Deploy production relay - if: github.repository == 'pingdotgg/t3code' runs-on: ubuntu-24.04 timeout-minutes: 15 environment: @@ -37,7 +37,22 @@ jobs: APNS_BUNDLE_ID: ${{ vars.APNS_BUNDLE_ID }} ALCHEMY_TELEMETRY_DISABLED: "1" steps: + - id: cloudflare_config + name: Detect Cloudflare configuration + shell: bash + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + run: | + if [[ -n "${CLOUDFLARE_ACCOUNT_ID:-}" && -n "${CLOUDFLARE_API_TOKEN:-}" ]]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "::notice::Relay deployment skipped because Cloudflare credentials are not configured." + - name: Checkout + if: steps.cloudflare_config.outputs.enabled == 'true' uses: actions/checkout@v6 with: sparse-checkout: | @@ -46,6 +61,7 @@ jobs: sparse-checkout-cone-mode: false - name: Setup Vite+ + if: steps.cloudflare_config.outputs.enabled == 'true' uses: voidzero-dev/setup-vp@v1 with: node-version-file: package.json @@ -55,6 +71,7 @@ jobs: - --filter=t3code-relay... - name: Deploy production relay stage + if: steps.cloudflare_config.outputs.enabled == 'true' id: deploy run: vp run --filter t3code-relay deploy --stage prod --yes --github-output env: @@ -66,6 +83,7 @@ jobs: APNS_PRIVATE_KEY: ${{ secrets.APNS_PRIVATE_KEY }} - name: Publish relay deploy commit status + if: steps.cloudflare_config.outputs.enabled == 'true' uses: actions/github-script@v8 with: script: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 487ab0ee195d..2a2cb784e579 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -83,6 +83,7 @@ jobs: is_prerelease: ${{ steps.release_meta.outputs.is_prerelease }} make_latest: ${{ steps.release_meta.outputs.make_latest }} ref: ${{ github.sha }} + connect_enabled: ${{ steps.connect_config.outputs.enabled }} steps: - name: Checkout uses: actions/checkout@v6 @@ -174,10 +175,25 @@ jobs: --current-tag "${{ steps.release_meta.outputs.tag }}" \ --github-output + - id: connect_config + name: Detect T3 Connect configuration + shell: bash + env: + CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + run: | + if [[ -n "${CLOUDFLARE_ACCOUNT_ID:-}" && -n "${CLOUDFLARE_API_TOKEN:-}" ]]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "::notice::T3 Connect public config is disabled because Cloudflare credentials are not configured." + relay_public_config: name: Resolve T3 Connect public config needs: preflight - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }} + if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.preflight.outputs.connect_enabled == 'true' }} runs-on: ubuntu-24.04 timeout-minutes: 5 environment: @@ -324,16 +340,25 @@ jobs: # build_wsl_node_pty, so a failed Linux prebuild doesn't skip the macOS/Linux # builds. `!cancelled()` (not `!failure()`) lets the job run even when # build_wsl_node_pty failed; the Windows-only download step below then fails - # that single platform if the prebuild is missing. + # that single platform if the prebuild is missing. A skipped relay config is + # also valid when this repository has not enabled T3 Connect. needs: [preflight, relay_public_config, build_wsl_node_pty] - if: ${{ !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }} + if: >- + ${{ + !cancelled() && + needs.preflight.result == 'success' && + ( + (needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.result == 'success') || + (needs.preflight.outputs.connect_enabled != 'true' && needs.relay_public_config.result == 'skipped') + ) + }} runs-on: ${{ matrix.runner }} timeout-minutes: 30 env: - T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} - T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} - T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} - T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }} + T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.clerk_publishable_key || '' }} + T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.clerk_jwt_template || '' }} + T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.clerk_cli_oauth_client_id || '' }} + T3CODE_RELAY_URL: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.relay_url || '' }} strategy: fail-fast: false matrix: @@ -398,12 +423,14 @@ jobs: targets: ${{ matrix.rust_target }} - name: Download relay client tracing config + if: needs.preflight.outputs.connect_enabled == 'true' uses: actions/download-artifact@v8 with: name: relay-client-tracing-config path: ${{ runner.temp }}/relay-client-tracing - name: Load relay client tracing config + if: needs.preflight.outputs.connect_enabled == 'true' shell: bash run: | config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" @@ -665,17 +692,26 @@ jobs: publish_cli: name: Publish CLI to npm needs: [preflight, relay_public_config, build] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build.result == 'success' }} + if: >- + ${{ + !failure() && !cancelled() && + needs.preflight.result == 'success' && + needs.build.result == 'success' && + ( + (needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.result == 'success') || + (needs.preflight.outputs.connect_enabled != 'true' && needs.relay_public_config.result == 'skipped') + ) + }} runs-on: ubuntu-24.04 # ubuntu-24.04 timeout-minutes: 10 permissions: contents: read id-token: write env: - T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} - T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} - T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} - T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }} + T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.clerk_publishable_key || '' }} + T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.clerk_jwt_template || '' }} + T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.clerk_cli_oauth_client_id || '' }} + T3CODE_RELAY_URL: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.relay_url || '' }} steps: - name: Checkout uses: actions/checkout@v6 @@ -698,12 +734,14 @@ jobs: - --filter=@t3tools/scripts... - name: Download relay client tracing config + if: needs.preflight.outputs.connect_enabled == 'true' uses: actions/download-artifact@v8 with: name: relay-client-tracing-config path: ${{ runner.temp }}/relay-client-tracing - name: Load relay client tracing config + if: needs.preflight.outputs.connect_enabled == 'true' shell: bash run: | config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" @@ -865,14 +903,23 @@ jobs: deploy_web: name: Deploy hosted web app needs: [preflight, relay_public_config, release] - if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.release.result == 'success' }} + if: >- + ${{ + !failure() && !cancelled() && + needs.preflight.result == 'success' && + needs.release.result == 'success' && + ( + (needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.result == 'success') || + (needs.preflight.outputs.connect_enabled != 'true' && needs.relay_public_config.result == 'skipped') + ) + }} runs-on: ubuntu-24.04 timeout-minutes: 10 env: - T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }} - T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }} - T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }} - T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }} + T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.clerk_publishable_key || '' }} + T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.clerk_jwt_template || '' }} + T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.clerk_cli_oauth_client_id || '' }} + T3CODE_RELAY_URL: ${{ needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.outputs.relay_url || '' }} VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} @@ -901,12 +948,14 @@ jobs: - --filter=@t3tools/web... - name: Download relay client tracing config + if: needs.preflight.outputs.connect_enabled == 'true' uses: actions/download-artifact@v8 with: name: relay-client-tracing-config path: ${{ runner.temp }}/relay-client-tracing - name: Load relay client tracing config + if: needs.preflight.outputs.connect_enabled == 'true' shell: bash run: | config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" @@ -1065,7 +1114,10 @@ jobs: if: | always() && !cancelled() && needs.preflight.result == 'success' && - needs.relay_public_config.result == 'success' && + ( + (needs.preflight.outputs.connect_enabled == 'true' && needs.relay_public_config.result == 'success') || + (needs.preflight.outputs.connect_enabled != 'true' && needs.relay_public_config.result == 'skipped') + ) && needs.release.result == 'success' && needs.deploy_web.result == 'success' && (needs.finalize.result == 'success' || needs.finalize.result == 'skipped') diff --git a/docs/operations/release.md b/docs/operations/release.md index 89c4006bafd5..2e5e73fa5d55 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -12,7 +12,8 @@ This document covers the unified release workflow for stable and nightly desktop - scheduled nightly check every three hours - manual `workflow_dispatch` for either channel - Runs quality gates first: lint, typecheck, test. -- Reads the shared production T3 Connect relay URL and Clerk client configuration before packaging clients. +- Reads the shared production T3 Connect relay URL and Clerk client configuration before packaging + clients when the repository has enabled Connect; otherwise builds artifacts without Connect. - Builds four artifacts in parallel for both channels: - macOS `arm64` DMG - macOS `x64` DMG @@ -46,8 +47,12 @@ them again in the finalize job, which can commit and push aligned package versio ## T3 Connect relay deployment The relay is a shared control plane versioned separately from client releases. Stable and nightly -client builds must point at the same relay so users see the same linked environments when switching -release channels. +client builds that enable Connect must point at the same relay so users see the same linked +environments when switching release channels. Repositories without Cloudflare credentials skip +relay resolution and build without Connect. + +See [Self-host the T3 Connect relay](./self-host-relay.md) for fork setup, Cloudflare token scopes, +first-deploy bootstrap, and release fallback behavior. `.github/workflows/deploy-relay.yml` deploys Alchemy stage `prod` on every push to `main`. The release workflow reads the relay URL and Clerk client configuration from the existing `production` diff --git a/docs/operations/self-host-relay.md b/docs/operations/self-host-relay.md new file mode 100644 index 000000000000..6dc9b183eec1 --- /dev/null +++ b/docs/operations/self-host-relay.md @@ -0,0 +1,242 @@ +# Self-host the T3 Connect relay + +> For operators of a T3 Code or T3 Turbo fork. + +This runbook deploys the existing T3 Connect relay into infrastructure owned by the fork operator +and explains how release builds consume it. T3 Connect remains optional: a repository without both +Cloudflare credentials produces release artifacts with Connect disabled. + +## Architecture + +The relay is a Cloudflare Worker that acts as the T3 Connect control plane. It authenticates users, +records linked environments, issues short-lived connection credentials, provisions managed +Cloudflare Tunnel endpoints and DNS records, and coordinates optional mobile notifications. + +The relay is not the data path for a connected environment. It helps a client discover and +authorize a connection; normal API and WebSocket traffic then flows directly between the client and +the environment's managed `cloudflared` endpoint. The relay therefore does not proxy terminal, +filesystem, or agent traffic. + +`infra/relay/alchemy.run.ts` provisions the Worker, queues, Hyperdrive connection, retained DNS +zones, runtime-scoped Cloudflare tokens, and Alchemy's Cloudflare state store. The production stage +also owns the relay database and tracing resources described in +[the relay README](../../infra/relay/README.md). + +## Prerequisites + +Before configuring GitHub, prepare: + +- A Cloudflare account with Workers, Queues, Hyperdrive, Secrets Store, and Cloudflare Tunnel + available. +- One or two active Cloudflare DNS zones. The relay API and managed tunnel endpoints may share a + zone, but the deployment still needs both `RELAY_API_ZONE_NAME` and + `RELAY_TUNNEL_ZONE_NAME`. +- A Clerk application for web, desktop, mobile, and CLI authentication. +- The current upstream relay's backing services: a PlanetScale Postgres organization, an Axiom + organization, and APNs credentials. These are not optional in the current + `infra/relay` stack even if a fork does not plan to use mobile notifications. +- A GitHub `production` Actions environment. The values in this runbook can be repository-level + variables and secrets; avoid defining stale values with the same names on the environment because + environment values take precedence. + +## Prepare Cloudflare + +### DNS zones + +Add the API zone and managed-tunnel zone to the target Cloudflare account and finish nameserver +activation before the first deploy. Production adopts these existing zones as retained Alchemy +resources; it does not own the registrar setup. + +- `RELAY_API_ZONE_NAME=example.com` produces `https://relay.example.com` by default. +- `RELAY_TUNNEL_ZONE_NAME=tunnels.example.com` produces per-environment hostnames below that + zone. +- `RELAY_DOMAIN` overrides only the relay API hostname. Leave it unset to use + `relay.`. + +The API hostname must not already have a conflicting CNAME. Cloudflare creates the Worker custom +domain and certificate during deployment. + +### Cloudflare API token + +Create one account-scoped token for GitHub Actions and restrict it to the selected Cloudflare +account and the API/tunnel zones. The minimum permission groups implied by the resources in +`infra/relay/alchemy.run.ts` are: + +Account permissions: + +- `Workers Scripts Write` +- `Queues Write` +- `Hyperdrive Write` +- `Secrets Store Write` +- `Account API Tokens Write` +- `Cloudflare Tunnel Read` +- `Cloudflare Tunnel Write` + +Zone permissions, restricted to the API and tunnel zones: + +- `Zone Read` +- `DNS Read` +- `DNS Write` + +Cloudflare's dashboard may display `Edit` where its API permission group uses +`Write`. `Account API Tokens Write` is required because Alchemy mints narrower tokens +for the Worker's runtime tunnel and DNS bindings. `Secrets Store Write` is also required on +read-state runs because Alchemy binds state-store secrets through a short-lived Worker preview. + +These permissions map to the Cloudflare APIs used by the stack: +[Workers scripts](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/), +[Queues](https://developers.cloudflare.com/api/resources/queues/), +[Hyperdrive](https://developers.cloudflare.com/api/resources/hyperdrive/), +[Secrets Store](https://developers.cloudflare.com/api/resources/secrets_store/), +[account-owned API tokens](https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/), +[Cloudflare Tunnel](https://developers.cloudflare.com/api/resources/zero_trust/subresources/tunnels/), +and [DNS records](https://developers.cloudflare.com/api/resources/dns/subresources/records/). + +## Prepare Clerk + +Use a single Clerk application for the relay and released clients. + +1. Copy its publishable key and secret key. +2. Create a JWT template named `t3-relay` with: + + ```json + { "aud": "t3-code-relay" } + ``` + +3. Create a public OAuth application for the CLI with PKCE. Enable + `openid`, `profile`, and `email` and allow: + + - `http://127.0.0.1:34338/callback` + - `/connect/callback` + +4. Enable Clerk's Native API and allow `t3code://app/` for packaged desktop builds. Add + development/mobile origins as needed for the surfaces the fork ships. + +Use `t3-relay` for `CLERK_JWT_TEMPLATE` and `t3-code-relay` for +`CLERK_JWT_AUDIENCE`. The OAuth application's public client ID becomes +`CLERK_CLI_OAUTH_CLIENT_ID`. Never expose `CLERK_SECRET_KEY` to a client build. + +See [T3 Connect](../internals/t3-connect.md) for the full Clerk redirect, native-auth, and passkey +configuration. + +## Configure GitHub + +Set these repository variables. The first six are the public self-host/release configuration: + +| Variable | Value | +| --- | --- | +| `CLOUDFLARE_ACCOUNT_ID` | Target Cloudflare account ID. This and the API token enable Connect in Release. | +| `RELAY_DOMAIN` | Optional explicit API hostname, for example `relay.example.com`. | +| `RELAY_API_ZONE_NAME` | Active Cloudflare zone containing the relay API hostname. | +| `CLERK_PUBLISHABLE_KEY` | Clerk application's public key. | +| `CLERK_JWT_TEMPLATE` | `t3-relay`, or the matching template name chosen by the operator. | +| `CLERK_CLI_OAUTH_CLIENT_ID` | Public client ID of the Clerk CLI OAuth application. | + +The unmodified upstream deployment also requires these repository variables: + +| Variable | Value | +| --- | --- | +| `RELAY_TUNNEL_ZONE_NAME` | Active zone used for managed environment endpoints; it may equal `RELAY_API_ZONE_NAME`. | +| `CLERK_JWT_AUDIENCE` | Audience from the JWT template, normally `t3-code-relay`. | +| `PLANETSCALE_ORGANIZATION` | PlanetScale organization containing the production relay database. | +| `AXIOM_ORG_ID` | Axiom organization for relay/client tracing. | +| `APNS_ENVIRONMENT` | `sandbox` or `production`. | +| `APNS_TEAM_ID` | Apple Developer team ID. | +| `APNS_KEY_ID` | APNs key ID. | +| `APNS_BUNDLE_ID` | App bundle ID used for notifications. | + +Set `CLOUDFLARE_API_TOKEN` as a repository secret. It is the only secret used as the +Release enablement gate. The deployment additionally requires these repository secrets: + +- `PLANETSCALE_API_TOKEN_ID` +- `PLANETSCALE_API_TOKEN` +- `AXIOM_TOKEN` +- `CLERK_SECRET_KEY` +- `APNS_PRIVATE_KEY` + +Example commands for the public release inputs: + +```sh +gh variable set CLOUDFLARE_ACCOUNT_ID --repo OWNER/REPO --body "" +gh variable set RELAY_API_ZONE_NAME --repo OWNER/REPO --body "example.com" +gh variable set RELAY_DOMAIN --repo OWNER/REPO --body "relay.example.com" +gh variable set CLERK_PUBLISHABLE_KEY --repo OWNER/REPO --body "pk_live_..." +gh variable set CLERK_JWT_TEMPLATE --repo OWNER/REPO --body "t3-relay" +gh variable set CLERK_CLI_OAUTH_CLIENT_ID --repo OWNER/REPO --body "" +gh secret set CLOUDFLARE_API_TOKEN --repo OWNER/REPO +``` + +Omit the `RELAY_DOMAIN` command when using the derived hostname. Set the deploy-only values +the same way before running the production deployment. + +## First deploy and later deploys + +The workflow [`deploy-relay.yml`](../../.github/workflows/deploy-relay.yml) supports both pushes +to `main` and manual dispatch: + +1. Merge the workflow to the default branch and add all variables and secrets above. +2. Open **Actions > Deploy T3 Connect relay > Run workflow** and select `main`. +3. Confirm **Detect Cloudflare configuration** reports enabled. +4. Confirm **Deploy production relay stage** completes and reports an HTTPS `relay_url`. + +The workflow uses `--stage prod --yes`. On a fresh Cloudflare account, `--yes` +non-interactively bootstraps Alchemy's `alchemy-state-store` Worker and Secrets Store before +deploying the relay. No separate local bootstrap command is required. The fork-specific repository +guard has been removed, so any fork with credentials can deploy. A repository without both +`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` gets a successful no-op workflow +instead of a failed deploy. + +After bootstrap, every push to `main` reconciles the `prod` stage. Re-running a failed +first deploy is safe because Alchemy adopts the retained zones and state-store resources. + +## How Release consumes the relay + +[`release.yml`](../../.github/workflows/release.yml) checks only +`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` to decide whether this repository +has enabled Connect. + +- If either is absent, **Resolve T3 Connect public config** is skipped. Desktop, CLI, and hosted-web + jobs receive empty Connect build variables and skip the relay tracing artifact, so installers can + still be built without Connect. +- If both are present, Release reads the deployed `prod` Alchemy state, uploads the + short-lived relay client tracing configuration, derives the public relay URL, and injects + `T3CODE_CLERK_PUBLISHABLE_KEY`, `T3CODE_CLERK_JWT_TEMPLATE`, + `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID`, and `T3CODE_RELAY_URL` into desktop, CLI, and + hosted-web builds. +- Once credentials enable the happy path, missing public variables, invalid credentials, or missing + production state remain hard failures. Release does not silently ship a Connect-free build when + an operator intended to enable it. + +Deploy the relay successfully before the first Connect-enabled release. + +## Verify + +1. Check readiness, including database connectivity: + + ```sh + curl --fail --show-error --silent "https:///health" + ``` + + Expected response: + + ```json + {"ok":true,"service":"relay"} + ``` + +2. In Cloudflare, confirm the relay Worker has its custom domain, two relay queues exist, + Hyperdrive points to the production database, and the Alchemy state-store Worker exists. +3. Inspect the next Release run: + - **Resolve T3 Connect public config** succeeds rather than skips. + - Desktop, CLI, and hosted-web jobs download the relay tracing artifact. +4. Install a resulting build, sign in under **Settings > Connections**, and link an environment. + `t3 connect status --json` should report the link and managed endpoint. +5. Connect from another client, then confirm ordinary API/WebSocket requests reach the managed + environment hostname directly rather than passing through the relay Worker. + +## Rebase safety + +Operator-owned state lives outside the fork: the Cloudflare account (including Alchemy state, +Workers, tunnels, DNS, queues, and Hyperdrive) and GitHub variables/secrets. Rebasing the fork cannot +delete that state. After resolving an upstream workflow conflict, confirm the credential gate, +manual relay dispatch, and fork-safe deployment behavior are still present before merging. Never +commit exported credentials or Alchemy state to make a rebase easier.