For maintainers. Using T3 Code? See docs/user.
The connection runtime is shared by web and mobile. It owns connectivity, authentication, retries, transport lifetime, cached environment data, and environment-scoped operations.
Web and mobile mount this runtime once at the application root and compose it
identically: apps/web/src/connection/runtime.ts and
apps/mobile/src/connection/runtime.ts differ only in the platform layer they
supply. There is no legacy connection owner or supported mixed mode.
connection/layer.ts assembles the runtime:
ConnectionResolver (resolver.ts) resolves a catalog entry into a
prepared, authenticated endpoint for primary, bearer, relay, or SSH targets.ConnectionDriver (driver.ts) prepares through the resolver, opens
one RPC session, and reports preparing, opening, and synchronizing.RpcSessionFactory (rpc/session.ts) performs one transport
attempt. It does not retry. RpcSession is the interface it returns,
exposing client, initialConfig, ready, probe, and closed.EnvironmentRegistry (registry.ts) owns the catalog and the
per-environment scopes.ConnectionOnboarding and RelayEnvironmentDiscovery sit alongside the
registry. Startup calls EnvironmentRegistry.start and streams platform
registrations into reconcilePlatform.The registry creates one environment-scoped supervisor per environment.
acquireSupervisor serializes access per environment, reuses an existing
supervisor when the catalog entry is unchanged, and closes and recreates the
scope when it changed. createServiceScope builds an EnvironmentSupervisor
bound to a closeable scope and connects it; run and runStream execute caller
effects with that supervisor provided.
EnvironmentSupervisor owns desired state, retry scheduling, and the active
session scope. React components do not create connections, transports, retry
loops, or RPC clients.
The supervisor is the only retry owner.
RETRY_DELAYS_MS). A connection stable for 30 seconds resets
accumulated backoff.Wakeup handling differs by phase, in supervisor.ts:
waitForEstablishmentInterrupt consumes and ignores
plain application activation. Restarting an in-flight attempt because the app
came to the foreground would only delay it. The exception is
application-active-reconnect, which mobile emits after a meaningful
background suspension; it interrupts establishment and resets the retry
ladder, because the OS may have silently killed the socket underneath the
attempt.monitorConnectedLease handles plain activation by probing
the existing session (lease.session.probe, with a shorter timeout for
mobile's application-active-probe) rather than reconnecting; a healthy
session survives foregrounding. application-active-reconnect skips the probe
and replaces the lease outright.The UI derives available, offline, connecting, reconnecting,
connected, and error from supervisor state plus explicit data-sync state.
It does not infer connection health from cached data or the existence of a
transport object. An environment becomes connected after the socket opens and
the initial config RPC succeeds, proving that the server is responsive. Shell
and thread synchronization are independent data states. A healthy RPC transport
with a failed shell subscription is shown as connected with a synchronization
error, not as a reconnect that is not actually scheduled.
Finite requests, durable subscriptions, and commands are separate APIs:
isTransportFailure: every failure is an RPC
client error) ends the inner subscription without resubscribing, so the outer
stream waits for the supervisor to supply a replacement session. A handled
domain failure runs onExpectedFailure and, when
retryExpectedFailureAfter is set, sleeps and resubscribes on the same
session. A healthy transport is never torn down for a domain failure.empty,
cached, synchronizing, or live, with a separate error field; there is
no failed status. Thread status adds deleted.src/state
(createProjectEnvironmentAtoms, createThreadEnvironmentAtoms), as are the
shell and thread state factories (createEnvironmentShellAtoms,
createEnvironmentThreadStateAtoms).The Promise bridge exists only at the React/Atom boundary. Runtime and business logic remain Effect-native.
Web and mobile provide:
Platform layers adapt operating-system capabilities. They do not implement
connection policy. EnvironmentOwnedDataCleanup is part of this contract: on
removal the registry clears its cache and calls the platform implementation, so
web clears composer drafts and mobile clears drafts plus the thread outbox.
Mobile cloud sign-out first saves relay drafts and queued messages in the local composer store under the owning account. These saved copies retain attachment files during cleanup and remain outside the active composer and upload queue. Signing back into that account restores them before relay credentials activate. Directly paired environments keep their drafts and outbox when cloud sign-out runs.
Mobile composer attachments upload over HTTP while their environment is connected, with at most three concurrent transfers. Drafts retain local image data or an owned file URI alongside the pending upload ID. Sending verifies and reuses that ID, or uploads the local bytes again if it expired. Disconnecting cancels active transfers without discarding drafts; reconnecting resumes preparation. Older servers without attachment-upload support continue to receive inline images.
Applications must import explicit package subpaths; the package intentionally
has no root export. The subpaths are documented in
packages/client-runtime/README.md,
with the exports map in that package's package.json as the authoritative
list. Files that are not exported are implementation details.
The application root mounts the shared connection layer, creates its own Atom runtime, and selects the domain atom factories required by that platform. Web and mobile may expose different hooks and features without changing connection ownership.
Application code must not construct RPC clients, retry loops, or raw orchestration commands. Persistence paths belong to the platform registration and cache stores, with explicit migration or invalidation policy.
Mounted detail consumers share one live thread subscription. The subscription ends when the last consumer leaves. Hidden routes that remain mounted still count as consumers.
A separate registry-local atom retains the last thread state and replay cursor for five idle minutes. A warm mount renders that state and resumes the stream without another snapshot download. The cache holds completed state and cursor updates together. It retains pagination data but clears canceled loading state. An ownership token prevents an old scope from replacing its successor's cache. Closing a thread skips snapshots that were already saved. Changed snapshots and failed background writes still get a final save attempt.
Core state-machine tests use @effect/vitest and deterministic service layers.
Required coverage includes: