| 1 | # Remote architecture |
| 2 | |
| 3 | Each connection joins a client to one environment over HTTP and WebSocket. The |
| 4 | environment owns providers, execution, files, and durable state. Direct access, |
| 5 | Tailscale, SSH, and T3 Connect change how the client reaches that server; they do |
| 6 | not introduce another execution model. See |
| 7 | [remote access](../user/remote-access.md) for setup. |
| 8 | |
| 9 | ## Identity is independent of the route |
| 10 | |
| 11 | An environment keeps its ID across server restarts and endpoint changes. Saved |
| 12 | connections are local to a client profile; the server's identity and state are |
| 13 | not. A repository identity can correlate clones across environments, but never |
| 14 | routes work between them. A project and its threads belong to one environment. |
| 15 | The canonical key follows the `upstream` remote when one exists, so pull request |
| 16 | features target the repository a fork tracks. A fork also reports its own |
| 17 | `origin`, and clients group and label by that, so a fork never collapses into a |
| 18 | checkout of its upstream. |
| 19 | |
| 20 | [Environment ID initialization](../../apps/server/src/environment/ServerEnvironment.ts) |
| 21 | must publish a complete ID atomically. Repair of an empty ID file retains a |
| 22 | recovery file so concurrent or delayed initializers choose the same winner. |
| 23 | Removing that recovery state as ordinary temporary-file cleanup can change the |
| 24 | identity underneath an already-running server. |
| 25 | |
| 26 | Advertised endpoints are reachability hints. Only the connecting device can |
| 27 | prove that a route works. In particular, a host's loopback address refers to a |
| 28 | different machine when another device opens it. Endpoint selection must not |
| 29 | silently fall back to loopback when a shareable endpoint is unavailable. |
| 30 | |
| 31 | A saved environment holds an ordered list of routes, and the |
| 32 | [driver](../../packages/client-runtime/src/connection/driver.ts) connects over |
| 33 | the first that works. Each direct route is first checked with the public |
| 34 | descriptor, so a saved LAN address that a different machine answers on another |
| 35 | network receives no credential. That check is not proof of a working route: |
| 36 | when every route stays silent, each is still tried. A route that fails to |
| 37 | connect, including a blocked one such as a signed-out T3 Connect, moves on to |
| 38 | the next; only an incompatible server stops the walk, because it is the same |
| 39 | server on every route. While connected over a later route the |
| 40 | [supervisor](../../packages/client-runtime/src/connection/supervisor.ts) |
| 41 | preflights the earlier ones and replaces the session when one would connect. |
| 42 | Preflight includes authorization so a route that answers but rejects this |
| 43 | client never costs a working session; a route that still fails afterwards is |
| 44 | held back for a cooldown so a flaky network cannot bounce the connection. |
| 45 | |
| 46 | A connected server reports the LAN and tailnet addresses it is bound to, and the |
| 47 | client saves them as learned routes. A learned route reuses the credential of |
| 48 | the route it was learned over: the T3 Connect access token, which is not bound |
| 49 | to an origin because each DPoP proof names the URL it signs, or the paired |
| 50 | bearer token. Learned routes the server stops reporting are dropped, which is |
| 51 | how a changed LAN address replaces the old one; routes the user saved are never |
| 52 | touched. The reported addresses are hints like any advertised endpoint, so a |
| 53 | learned route still has to answer as this environment before it is used. |
| 54 | |
| 55 | GitHub routing trust covers the whole route list. Adding or changing a route |
| 56 | revokes it; reordering does not, because the same addresses remain trusted. |
| 57 | |
| 58 | ## Hosted web is a client |
| 59 | |
| 60 | The hosted web app stores its connection catalog in the browser and connects |
| 61 | directly to each environment. It does not proxy traffic or hold server-side |
| 62 | pairing state. Hosting the UI over HTTPS therefore cannot make a plain HTTP LAN |
| 63 | backend accessible from that browser context. |
| 64 | |
| 65 | A [hosted pairing URL](../../apps/web/src/hostedPairing.ts) identifies the backend |
| 66 | in its query and carries the pairing secret in its fragment. Fragments stay out |
| 67 | of requests to the hosted origin. The browser exchanges the secret with the |
| 68 | environment and strips it from its history. Moving the token into a query |
| 69 | parameter would disclose it to the wrong origin. |
| 70 | |
| 71 | ## Access and process ownership are different |
| 72 | |
| 73 | Tailscale supplies an endpoint for ordinary pairing, so it needs no separate |
| 74 | environment type. Authentication remains the environment's responsibility for |
| 75 | every route. See [environment authentication](./environment-auth.md) and the |
| 76 | [T3 Connect trust boundary](./t3-connect.md). |
| 77 | |
| 78 | SSH can launch a server as well as forward a port. Desktop main owns that |
| 79 | lifecycle because it can spawn SSH and handle authentication prompts. The |
| 80 | renderer uses the forwarded endpoint through the shared connection runtime. |
| 81 | [SSH cleanup](../../packages/ssh/src/tunnel.ts) stops a remote server only if the |
| 82 | launcher owns it; a server it discovered already running must survive a client |
| 83 | disconnect. Reconnection restores the forward before opening the application |
| 84 | transport. |
| 85 | |
| 86 | Remote servers can outlive several client releases. Clients must use advertised |
| 87 | capabilities and handle their absence, rather than assume their own version |
| 88 | describes the server. Process replacement belongs to the launcher's |
| 89 | [update protocol](./server-updates.md); the connection runtime handles the |
| 90 | resulting disconnect. |
| 91 | |
| 92 | ### Desktop without a local environment |
| 93 | |
| 94 | Desktop normally launches its own primary server, but the desktop setting `localEnvironmentEnabled` |
| 95 | (`apps/desktop/src/settings/DesktopAppSettings.ts`) turns that off. Changing it relaunches the app; |
| 96 | no local state is deleted. On the next start the main process skips port selection, server exposure, |
| 97 | and the primary and WSL backends, and opens the window right away. The renderer sees this through |
| 98 | `desktopBridge.getLocalEnvironmentEnabled()`: `readPrimaryEnvironmentTarget` returns null, so primary |
| 99 | auth and platform-managed discovery are skipped and only saved environments (pairing, relay, SSH) |
| 100 | connect. This is possible because the desktop renderer is not served by the backend: the `t3code://` |
| 101 | scheme serves the bundled client from disk (Vite in development) and API traffic always goes to the |
| 102 | environment's own URL. |