How the desktop app, SSH environments, Tailscale exposure and T3 Connect fit together — from a real debugging session, ending with a fully automated rollout recipe.
The app opened to a red error: PrimaryEnvironmentRequestError caused by DesktopLocalEnvironmentAuthSessionBootstrapError — "Failed to create the local desktop bearer session." The UI was dead on arrival.
The desktop app spawns a local backend server and must exchange a bootstrap token with it before the UI can do anything. That server was crash-looping on startup while replaying its event store (~/.t3/userdata/state.sqlite):
PersistenceDecodeError: Decode error in OrchestrationEventStore.readFromSequence
[cause]: SchemaError: Expected "user" | "assistant" | "system"
at ["payload"]["role"]
26 rows in orchestration_events had payload.role = "reasoning" — written during a session that used a Grok model. Version 0.0.42's replay schema only accepts user | assistant | system, so the server died the instant it read those rows.
flowchart LR A["Desktop app launches"] --> B["Spawn local server
bin.mjs --bootstrap-fd 3"] B --> C["Replay orchestration_events
from state.sqlite"] C --> D{"row with
role = reasoning?"} D -- "yes (26 rows)" --> E["PersistenceDecodeError
server exits code 1"] E --> F["Token exchange never happens"] F --> G["UI error:
PrimaryEnvironmentRequestError"] E -. "desktop retries" .-> B style E fill:#3d1420,stroke:#fc8181,color:#ffd7d7 style G fill:#3d1420,stroke:#fc8181,color:#ffd7d7
state.sqlite (never surgery without a backup).200, UI loads.role: "reasoning" → "assistant") instead of a full reset — keeps all thread history. The full reset trades history for certainty.~/.t3/userdata/logs/server-child.log.Every "environment" in T3 Code is an independent t3 server. The desktop app is a shell that manages connections to one or more of them — the local one, plus remote ones over SSH.
flowchart TB
subgraph MAC["Mac — Desktop app (Electron)"]
UI["Renderer UI"]
MAIN["Main process
DesktopBackendPool"]
UI <-->|"desktop:* IPC"| MAIN
end
subgraph LOCAL["Primary environment (local)"]
LS["t3 server
127.0.0.1:3773"]
end
subgraph PC["PC — SSH environment"]
RS["t3 server
127.0.0.1:3773 (localhost-only)"]
end
MAIN <-->|"bearer token + HTTP/WS"| LS
MAIN <-->|"SSH tunnel (port forward)"| RS
LS --- DB1[("state.sqlite
~/.t3/userdata")]
RS --- DB2[("state.sqlite
~/.t3/userdata")]
style MAC fill:#17182a,stroke:#7c6cf8,color:#e8e8f0
style PC fill:#122626,stroke:#4fd1c5,color:#e8e8f0
The desktop auto-discovers hosts from ~/.ssh/config. Picking one triggers a fully automated provisioning sequence — no manual setup on the remote.
sequenceDiagram
autonumber
participant U as User
participant D as Desktop app
participant S as PC (SSH)
participant G as GitHub Releases
U->>D: Add environment → SSH → pick "pc"
D->>S: ssh run remote launch script (sh -l -s)
S->>G: download matching t3 runtime archive
G-->>S: runtime
S->>S: pick free port, start server on 127.0.0.1:3773
S-->>D: { remotePort: 3773, kind: "managed" }
D->>S: ssh issue pairing token
S-->>D: pairing credential
D->>S: bootstrap bearer session
D->>D: open tunnel + register backend in pool
Note over D,S: Environment shows "Connected"
| Check | Why it matters | Result |
|---|---|---|
ssh host works in BatchMode | The app can't answer passphrase prompts | ✓ |
Remote node meets engine range | Runtime executes via node | ✓ v26.8.2 |
curl / wget / tar present | Runtime download & unpack | ✓ |
| Remote can reach GitHub releases | Runtime download source | ✓ |
sh -l login shell works | Launch script runs via login shell | ✓ |
connect to host 192.168.x.x port 22: Operation timed out — the Mac had moved from home Wi-Fi to a hotspot, so the LAN alias was unreachable. Switching to the host's Tailscale alias (reachable on any network) fixed it instantly.Does the SSH environment appear on iOS automatically? No. The iOS app is a separate client, and the remote server binds 127.0.0.1 only — nothing off-box can reach it. Two changes make it work:
tailscale serve --bg 3773 → https://<host>.<tailnet>.ts.nett3 auth pairing create --ttl 30d --base-url https://<host>.<tailnet>.ts.net --json/pair#token=… URL.
flowchart LR
subgraph PHONE["iPhone"]
IOS["T3 Code iOS app"]
TS1["Tailscale app"]
end
subgraph NET["Tailnet (WireGuard)"]
direction LR
SERVE["tailscale serve
https://host.tailnet.ts.net"]
end
subgraph PC2["PC"]
RS2["t3 server
127.0.0.1:3773"]
end
IOS -->|pair URL + bearer| SERVE
TS1 -. encrypted tunnel .- SERVE
SERVE -->|proxy| RS2
style PHONE fill:#17182a,stroke:#7c6cf8,color:#e8e8f0
style PC2 fill:#122626,stroke:#4fd1c5,color:#e8e8f0
Spinning up a couple of servers every week? Pairing links don't scale — iOS has no way to auto-import them. The designed mechanism for this is T3 Connect: link a server to your account once, and it appears on every signed-in device by itself.
flowchart TB
subgraph NEW["New server (weekly)"]
P1["1. install t3 runtime"]
P2["2. t3 connect link --headless
(one-tap OAuth approve)"]
P3["3. t3 service install +
t3 serve --mode web"]
P1 --> P2 --> P3
end
RELAY["T3 Connect managed relay
(t3coderelay)"]
subgraph DEVICES["Your devices — automatic"]
D1["Mac desktop"]
D2["iPhone"]
D3["iPad / web"]
end
P3 -->|registers via relay| RELAY
RELAY --> D1 & D2 & D3
style NEW fill:#122626,stroke:#4fd1c5,color:#e8e8f0
style RELAY fill:#2a2140,stroke:#7c6cf8,color:#e8e8f0
style DEVICES fill:#17182a,stroke:#7c6cf8,color:#e8e8f0
| Pairing link (manual) | T3 Connect (automatic) | |
|---|---|---|
| Per-server work on iOS | Paste URL every time | None — appears in list |
| Works off-tailnet | No | Yes (relay over internet) |
| Survives reboot | No (desktop-managed) | Yes (systemd service) |
| Push notifications / Live Activities | No | Yes (connect publish) |
| Manual step remaining | Everything | One OAuth tap per server |
Drop this into your server setup routine (cloud-init, Ansible, or a bootstrap script). ~30 seconds of human time per server — one OAuth approval — and the environment shows up on all your devices on its own.
# 1. Install the t3 runtime (check releases for latest)
curl -fsSL https://github.com/pingdotgg/t3code/releases/latest/download/t3-linux-x64.tar.gz | tar xz
sudo install -m 755 t3 /usr/local/bin/t3
# 2. Link to T3 Connect (headless OAuth device flow)
# → prints a code + URL; approve once from any browser
t3 connect link --headless
# 3. Install as a background service (auto-starts on boot)
t3 service install
# 4. Run headless and join the managed relay
t3 serve --mode web
# 5. Optional: enable mobile push / Live Activities
t3 connect publish
# Done — the environment now appears automatically in the
# iOS app (and desktop) under T3 Connect. Verify with:
t3 connect status
| Symptom | Where to look |
|---|---|
| Desktop UI dead, bearer session error | ~/.t3/userdata/logs/server-child.log — is the local server crash-looping? |
| SSH environment won't add | Run the pre-flight table from Chapter 3; check reachability of the alias right now (network changes!) |
| iOS can't reach server | tailscale serve status on the server; Tailscale connected on the phone? |
| Environment gone after reboot | You want t3 service install — desktop-managed servers don't auto-start |
~/.t3/userdata/state.sqlite · logs in ~/.t3/userdata/logs/ · desktop-managed SSH launchers in ~/.t3/ssh-launch/<key>/.