T3 Code
Remote Environments Field Guide

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.

T3 Code (Alpha) 0.0.42 macOS · Linux · iOS SSH · Tailscale · T3 Connect
Chapter 1

The crash: a one-byte schema mismatch

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.

What was actually happening

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
Fig. 1 — The crash loop: replay fails → no bearer session → dead UI → respawn → repeat.

The fix

  1. Quit the app fully — it was crash-looping and holding the database.
  2. Back up state.sqlite (never surgery without a backup).
  3. Reset the database — moved aside; the server regenerated a clean one on next launch.
  4. Relaunch → server healthy on port 3773, bearer bootstrap returns 200, UI loads.
Alternative fix (surgical): patch the 26 rows (role: "reasoning" → "assistant") instead of a full reset — keeps all thread history. The full reset trades history for certainty.
Root cause is an app bug: a provider path writes events the reader can't decode. If you use reasoning-capable models on 0.0.42, this can recur — keep an eye on ~/.t3/userdata/logs/server-child.log.
Chapter 2

How the pieces fit together

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
Fig. 2 — Two servers, two databases. The desktop main process authenticates to each with its own bearer token.
Key insight: environments don't share state and don't sync. Each client (desktop, iOS) keeps its own connection catalog and pairs with each server individually. This fact drives everything that follows.
Chapter 3

Adding the SSH environment

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"
      
Fig. 3 — Managed SSH provisioning: the desktop installs, launches, pairs and tunnels — all over two SSH calls.

Pre-flight checklist (verified before adding)

CheckWhy it mattersResult
ssh host works in BatchModeThe app can't answer passphrase prompts
Remote node meets engine rangeRuntime executes via node✓ v26.8.2
curl / wget / tar presentRuntime download & unpack
Remote can reach GitHub releasesRuntime download source
sh -l login shell worksLaunch script runs via login shell
Gotcha that bit us: the first attempt failed with 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.
Chapter 4

Getting it on the iOS app

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:

  1. Expose the server on the tailnet (server stays localhost-only; Tailscale adds HTTPS + tailnet-only access):
    tailscale serve --bg 3773https://<host>.<tailnet>.ts.net
  2. Mint a pairing link on the server:
    t3 auth pairing create --ttl 30d --base-url https://<host>.<tailnet>.ts.net --json
  3. On the iPhone: Tailscale app connected → T3 Code → Add environment → Remote link → paste the /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
Fig. 4 — iPhone talks straight to the PC over the tailnet. The Mac is no longer in the path.
Independence achieved: after pairing, iOS talks directly to the PC — the Mac doesn't need to be awake. One caveat: the desktop-managed server doesn't auto-start if the PC reboots; opening the desktop app once relaunches it (Chapter 6 removes this caveat entirely).
Chapter 5

Scaling: zero-touch environments with T3 Connect

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
Fig. 5 — Linked servers self-register on the relay; every signed-in client sees them. No codes, no ports, no Tailscale required on the phone.

What you get vs. the manual path

Pairing link (manual)T3 Connect (automatic)
Per-server work on iOSPaste URL every timeNone — appears in list
Works off-tailnetNoYes (relay over internet)
Survives rebootNo (desktop-managed)Yes (systemd service)
Push notifications / Live ActivitiesNoYes (connect publish)
Manual step remainingEverythingOne OAuth tap per server
Chapter 6

The provisioning runbook

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

Troubleshooting quick reference

SymptomWhere to look
Desktop UI dead, bearer session error~/.t3/userdata/logs/server-child.log — is the local server crash-looping?
SSH environment won't addRun the pre-flight table from Chapter 3; check reachability of the alias right now (network changes!)
iOS can't reach servertailscale serve status on the server; Tailscale connected on the phone?
Environment gone after rebootYou want t3 service install — desktop-managed servers don't auto-start
File map: server state lives in ~/.t3/userdata/state.sqlite · logs in ~/.t3/userdata/logs/ · desktop-managed SSH launchers in ~/.t3/ssh-launch/<key>/.