Skip to content

Commit 5ae77c0

Browse files
feat(relay): Add managed relay tunnels and APN service (#2837)
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent 300f7fd commit 5ae77c0

338 files changed

Lines changed: 48977 additions & 2064 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Optional: T3 Cloud source builds
2+
# Leave these unset to disable optional T3 Cloud features in local source builds.
3+
# Release builds inject their public values at build time. Do not add server-side
4+
# secrets to this file.
5+
6+
# Get these from the Clerk Dashboard under API keys, JWT templates, and OAuth applications.
7+
# T3CODE_CLERK_PUBLISHABLE_KEY=pk_test_...
8+
# T3CODE_CLERK_JWT_TEMPLATE=t3-relay
9+
# T3CODE_CLERK_CLI_OAUTH_CLIENT_ID=oauthapp_...
10+
11+
# Get this from your relay deployment. `infra/relay` deploys update it automatically.
12+
# T3CODE_RELAY_URL=https://relay.example.com

.github/workflows/deploy-relay.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Deploy T3 Cloud relay
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
contents: read
10+
id-token: none
11+
12+
concurrency:
13+
group: relay-production
14+
cancel-in-progress: false
15+
16+
jobs:
17+
deploy_relay:
18+
name: Deploy production relay
19+
runs-on: blacksmith-8vcpu-ubuntu-2404
20+
timeout-minutes: 15
21+
environment:
22+
name: production
23+
env:
24+
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
25+
PLANETSCALE_ORGANIZATION: ${{ vars.PLANETSCALE_ORGANIZATION }}
26+
AXIOM_ORG_ID: ${{ vars.AXIOM_ORG_ID }}
27+
RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }}
28+
RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }}
29+
RELAY_TUNNEL_ZONE_NAME: ${{ vars.RELAY_TUNNEL_ZONE_NAME }}
30+
CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
31+
CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }}
32+
APNS_ENVIRONMENT: ${{ vars.APNS_ENVIRONMENT }}
33+
APNS_TEAM_ID: ${{ vars.APNS_TEAM_ID }}
34+
APNS_KEY_ID: ${{ vars.APNS_KEY_ID }}
35+
APNS_BUNDLE_ID: ${{ vars.APNS_BUNDLE_ID }}
36+
ALCHEMY_TELEMETRY_DISABLED: "1"
37+
steps:
38+
- name: Checkout
39+
uses: actions/checkout@v6
40+
41+
- name: Setup Vite+
42+
uses: voidzero-dev/setup-vp@v1
43+
with:
44+
node-version-file: package.json
45+
cache: true
46+
run-install: false
47+
48+
- name: Install dependencies
49+
run: vp install --frozen-lockfile
50+
51+
- name: Deploy production relay stage
52+
run: vp run --filter t3code-relay deploy -- --stage prod --yes
53+
env:
54+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
55+
PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }}
56+
PLANETSCALE_API_TOKEN: ${{ secrets.PLANETSCALE_API_TOKEN }}
57+
AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }}
58+
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
59+
APNS_PRIVATE_KEY: ${{ secrets.APNS_PRIVATE_KEY }}

.github/workflows/release.yml

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,12 +166,69 @@ jobs:
166166
--current-tag "${{ steps.release_meta.outputs.tag }}" \
167167
--github-output
168168
169-
build:
170-
name: Build ${{ matrix.label }}
169+
relay_public_config:
170+
name: Resolve T3 Cloud public config
171171
needs: preflight
172172
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' }}
173+
runs-on: blacksmith-8vcpu-ubuntu-2404
174+
timeout-minutes: 5
175+
environment:
176+
name: production
177+
outputs:
178+
clerk_publishable_key: ${{ steps.public_config.outputs.clerk_publishable_key }}
179+
clerk_jwt_template: ${{ steps.public_config.outputs.clerk_jwt_template }}
180+
clerk_cli_oauth_client_id: ${{ steps.public_config.outputs.clerk_cli_oauth_client_id }}
181+
relay_url: ${{ steps.public_config.outputs.relay_url }}
182+
env:
183+
RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }}
184+
RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }}
185+
CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }}
186+
CLERK_JWT_TEMPLATE: ${{ vars.CLERK_JWT_TEMPLATE }}
187+
CLERK_CLI_OAUTH_CLIENT_ID: ${{ vars.CLERK_CLI_OAUTH_CLIENT_ID }}
188+
steps:
189+
- id: public_config
190+
name: Resolve production relay public config
191+
shell: bash
192+
run: |
193+
set -euo pipefail
194+
195+
relay_domain="${RELAY_DOMAIN:-}"
196+
if [[ -z "$relay_domain" && -n "${RELAY_API_ZONE_NAME:-}" ]]; then
197+
relay_domain="relay.$RELAY_API_ZONE_NAME"
198+
fi
199+
required=(
200+
relay_domain
201+
CLERK_PUBLISHABLE_KEY
202+
CLERK_JWT_TEMPLATE
203+
CLERK_CLI_OAUTH_CLIENT_ID
204+
)
205+
missing=()
206+
for name in "${required[@]}"; do
207+
if [[ -z "${!name:-}" ]]; then
208+
missing+=("$name")
209+
fi
210+
done
211+
if (( ${#missing[@]} > 0 )); then
212+
printf 'Missing required relay deployment configuration: %s\n' "${missing[*]}" >&2
213+
exit 1
214+
fi
215+
216+
echo "clerk_publishable_key=$CLERK_PUBLISHABLE_KEY" >> "$GITHUB_OUTPUT"
217+
echo "clerk_jwt_template=$CLERK_JWT_TEMPLATE" >> "$GITHUB_OUTPUT"
218+
echo "clerk_cli_oauth_client_id=$CLERK_CLI_OAUTH_CLIENT_ID" >> "$GITHUB_OUTPUT"
219+
echo "relay_url=https://$relay_domain" >> "$GITHUB_OUTPUT"
220+
221+
build:
222+
name: Build ${{ matrix.label }}
223+
needs: [preflight, relay_public_config]
224+
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' }}
173225
runs-on: ${{ matrix.runner }}
174226
timeout-minutes: 30
227+
env:
228+
T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
229+
T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
230+
T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
231+
T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
175232
strategy:
176233
fail-fast: false
177234
matrix:
@@ -422,13 +479,18 @@ jobs:
422479

423480
publish_cli:
424481
name: Publish CLI to npm
425-
needs: [preflight, build]
426-
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.build.result == 'success' }}
482+
needs: [preflight, relay_public_config, build]
483+
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.build.result == 'success' }}
427484
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
428485
timeout-minutes: 10
429486
permissions:
430487
contents: read
431488
id-token: write
489+
env:
490+
T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
491+
T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
492+
T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: ${{ needs.relay_public_config.outputs.clerk_cli_oauth_client_id }}
493+
T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
432494
steps:
433495
- name: Checkout
434496
uses: actions/checkout@v6
@@ -577,11 +639,14 @@ jobs:
577639

578640
deploy_web:
579641
name: Deploy hosted web app
580-
needs: [preflight, release]
581-
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }}
642+
needs: [preflight, relay_public_config, release]
643+
if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.relay_public_config.result == 'success' && needs.release.result == 'success' }}
582644
runs-on: blacksmith-8vcpu-ubuntu-2404
583645
timeout-minutes: 10
584646
env:
647+
T3CODE_CLERK_PUBLISHABLE_KEY: ${{ needs.relay_public_config.outputs.clerk_publishable_key }}
648+
T3CODE_CLERK_JWT_TEMPLATE: ${{ needs.relay_public_config.outputs.clerk_jwt_template }}
649+
T3CODE_RELAY_URL: ${{ needs.relay_public_config.outputs.relay_url }}
585650
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
586651
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
587652
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
@@ -648,6 +713,9 @@ jobs:
648713
--token "$VERCEL_TOKEN" \
649714
"${vercel_scope_args[@]}" \
650715
--build-env "APP_VERSION=${{ needs.preflight.outputs.version }}" \
716+
--build-env "T3CODE_CLERK_PUBLISHABLE_KEY=${T3CODE_CLERK_PUBLISHABLE_KEY:-}" \
717+
--build-env "T3CODE_CLERK_JWT_TEMPLATE=${T3CODE_CLERK_JWT_TEMPLATE:-}" \
718+
--build-env "T3CODE_RELAY_URL=${T3CODE_RELAY_URL:-}" \
651719
--build-env "VITE_HOSTED_APP_URL=$router_url" \
652720
--build-env "VITE_HOSTED_APP_CHANNEL=$channel_name"
653721
)"
@@ -744,10 +812,11 @@ jobs:
744812
if: |
745813
always() && !cancelled() &&
746814
needs.preflight.result == 'success' &&
815+
needs.relay_public_config.result == 'success' &&
747816
needs.release.result == 'success' &&
748817
needs.deploy_web.result == 'success' &&
749818
(needs.finalize.result == 'success' || needs.finalize.result == 'skipped')
750-
needs: [preflight, release, deploy_web, finalize]
819+
needs: [preflight, relay_public_config, release, deploy_web, finalize]
751820
runs-on: blacksmith-8vcpu-ubuntu-2404
752821
timeout-minutes: 10
753822
steps:

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ node_modules
55
*.log
66
*.tsbuildinfo
77
apps/*/dist
8+
infra/*/dist
89
.astro
910
packages/*/dist
1011
.env
@@ -26,3 +27,8 @@ squashfs-root/
2627
.gstack/
2728
dist-electron/
2829
.electron-runtime/
30+
node_modules/
31+
.alchemy/
32+
*.log
33+
.env*
34+
!.env.example

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
},
77
"oxc.unusedDisableDirectives": "warn",
88
"js/ts.tsdk.path": "node_modules/typescript/lib",
9+
"typescript.native-preview.tsdk": "node_modules/@typescript/native-preview",
910
"typescript.preferences.autoImportFileExcludePatterns": [".repos/**"],
1011
"javascript.preferences.autoImportFileExcludePatterns": [".repos/**"],
1112
"files.watcherExclude": {

AGENTS.md

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,8 @@ Long term maintainability is a core priority. If you add new functionality, firs
2929
- `apps/server`: Node.js WebSocket server. Wraps Codex app-server (JSON-RPC over stdio), serves the React web app, and manages provider sessions.
3030
- `apps/web`: React/Vite UI. Owns session UX, conversation/event rendering, and client-side state. Connects to the server via WebSocket.
3131
- `packages/contracts`: Shared effect/Schema schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. Keep this package schema-only — no runtime logic.
32-
- `packages/shared`: Shared runtime utilities consumed by both server and web. Uses explicit subpath exports (e.g. `@t3tools/shared/git`) — no barrel index.
33-
34-
## Codex App Server (Important)
35-
36-
T3 Code is currently Codex-first. The server starts `codex app-server` (JSON-RPC over stdio) per provider session, then streams structured events to the browser through WebSocket push messages.
37-
38-
How we use it in this codebase:
39-
40-
- Session startup/resume and turn lifecycle are brokered in `apps/server/src/codexAppServerManager.ts`.
41-
- Provider dispatch and thread event logging are coordinated in `apps/server/src/providerManager.ts`.
42-
- WebSocket server routes NativeApi methods in `apps/server/src/wsServer.ts`.
43-
- Web app consumes orchestration domain events via WebSocket push on channel `orchestration.domainEvent` (provider runtime activity is projected into orchestration events server-side).
44-
45-
Docs:
46-
47-
- Codex App Server docs: https://developers.openai.com/codex/sdk/#app-server
32+
- `packages/shared`: Shared runtime utilities consumed by both server and client applications. Uses explicit subpath exports (e.g. `@t3tools/shared/git`) — no barrel index.
33+
- `packages/client-runtime`: Shared runtime package for sharing client code across web and mobile.
4834

4935
## Reference Repos
5036

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ We are not accepting contributions yet.
4848

4949
Observability guide: [docs/observability.md](./docs/observability.md)
5050

51+
Relay observability: [docs/relay-observability.md](./docs/relay-observability.md)
52+
53+
T3 Cloud Clerk setup: [docs/t3-cloud-clerk.md](./docs/t3-cloud-clerk.md)
54+
5155
## If you REALLY want to contribute still.... read this first
5256

5357
Before local development, prepare the environment and install dependencies:
@@ -58,6 +62,10 @@ mise install
5862
vp install
5963
```
6064

65+
T3 Cloud is optional and disabled in a fresh clone. To enable it for web, desktop, and mobile source
66+
builds, copy [`.env.example`](./.env.example) to `.env` at the repository root and set the canonical
67+
public configuration there.
68+
6169
Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening an issue or PR.
6270

6371
Need support? Join the [Discord](https://discord.gg/jn4EGJjrvv).

apps/desktop/scripts/dev-electron.mjs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
22
import { watch } from "node:fs";
33
import { join } from "node:path";
44

5-
import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";
5+
import { desktopDir, resolveDevProtocolClient, resolveElectronPath } from "./electron-launcher.mjs";
66
import { waitForResources } from "./wait-for-resources.mjs";
77

88
const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim();
@@ -28,6 +28,7 @@ const watchedDirectories = [
2828
const forcedShutdownTimeoutMs = 1_500;
2929
const restartDebounceMs = 120;
3030
const childTreeGracePeriodMs = 1_200;
31+
const remoteDebuggingPort = process.env.T3CODE_DESKTOP_REMOTE_DEBUGGING_PORT?.trim();
3132

3233
await waitForResources({
3334
baseDir: desktopDir,
@@ -38,6 +39,11 @@ await waitForResources({
3839

3940
const childEnv = { ...process.env };
4041
delete childEnv.ELECTRON_RUN_AS_NODE;
42+
const devProtocolClient = resolveDevProtocolClient();
43+
if (devProtocolClient) {
44+
childEnv.T3CODE_DESKTOP_APP_USER_MODEL_ID = devProtocolClient.appBundleId;
45+
childEnv.T3CODE_DESKTOP_PROTOCOL_REGISTRATION_MANAGED = "1";
46+
}
4147

4248
let shuttingDown = false;
4349
let restartTimer = null;
@@ -67,15 +73,17 @@ function startApp() {
6773
return;
6874
}
6975

70-
const app = spawn(
71-
resolveElectronPath(),
72-
[`--t3code-dev-root=${desktopDir}`, "dist-electron/main.cjs"],
73-
{
74-
cwd: desktopDir,
75-
env: childEnv,
76-
stdio: "inherit",
77-
},
78-
);
76+
const electronArgs = remoteDebuggingPort
77+
? [`--remote-debugging-port=${remoteDebuggingPort}`]
78+
: [];
79+
const launchArgs = devProtocolClient
80+
? electronArgs
81+
: [...electronArgs, `--t3code-dev-root=${desktopDir}`, "dist-electron/main.cjs"];
82+
const app = spawn(resolveElectronPath(), launchArgs, {
83+
cwd: desktopDir,
84+
env: childEnv,
85+
stdio: "inherit",
86+
});
7987

8088
currentApp = app;
8189

@@ -125,6 +133,7 @@ async function stopApp() {
125133
app.once("exit", finish);
126134
app.kill("SIGTERM");
127135
killChildTreeByPid(app.pid, "TERM");
136+
cleanupStaleDevApps();
128137

129138
setTimeout(() => {
130139
if (settled) {
@@ -133,6 +142,7 @@ async function stopApp() {
133142

134143
app.kill("SIGKILL");
135144
killChildTreeByPid(app.pid, "KILL");
145+
cleanupStaleDevApps();
136146
finish();
137147
}, forcedShutdownTimeoutMs).unref();
138148
});

0 commit comments

Comments
 (0)