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