Skip to main content

Conn layer

This page is for the person who builds the Conn client, and for the Gateway maintainer. Conn is the fork of T3 Code that works with Orbit. The Gateway gives Conn one central layer: a device signs in as its Node, sees every registered T3 server, and shares workspaces, appearance, and device settings with the other devices of its profile. T3 traffic does not pass through the Gateway. A device talks to each T3 server directly over WireGuard. The Gateway answers lists, profiles, and pairing. It mints T3 pairing links with an admin session that each T3 server hands it at registration, and it revokes the sessions it paired.

Identity

A caller is the active Node that owns its WireGuard address, as for every other Gateway API call. Phones and laptops are Nodes too, without roles, much like an operator Node. A caller that is not an active Node gets peer.identity_unknown (403). This slice has no grants. The Conn endpoints need no node access edge: every active Node may read and change every profile, register a T3 server, mint a pairing link, and revoke any Node’s sessions.

Profiles

A profile is a named set of shared settings, such as “Nick”. A Node belongs to at most one profile. On first connect the device lists the profiles, lets the user pick or create one, and binds to it. Later calls read the binding from GET /api/v1/conn/me. Every Node sees every profile. A profile holds one settings document. Its keys follow Conn’s stores, so they stay camelCase. The document has three kinds of section:
A section is workspaces, appearance, or devices.<type>. The document accepts no other top-level key. Only workspaces is always present; a section that no device wrote is absent. A device type is 2 to 24 lowercase letters, digits, and hyphens, and starts with a letter, such as desktop or iphone. A theme slot has a required id and a required source: stock, builtin, custom, or published. It may have a name, an appearance (light or dark), and colors. A stock or builtin slot needs only its id. A custom or published slot carries its resolved colors, so another device can draw a theme it does not have. A custom theme has a name, an appearance, colors, and an id that is unique in the list. It may have variants.light and variants.dark, which are color maps. A color map has at most 80 entries. A key is a color role: a letter, then letters and digits, at most 64 characters. A value is a string of at most 64 characters from #A-Za-z0-9(),.%/ -, so it cannot carry markup or a url. A device values map has camelCase keys of at most 64 characters. A value is a boolean, a number, null, a string of at most 512 characters, or a list of at most 32 of those scalars. The Gateway checks the envelope and the value types. Each client checks its own keys and keeps the ones it does not know. An empty object is refused wherever a map is expected, such as colors, values, or the devices map of a PUT. PHP cannot tell {} from [], so the Gateway would hand it back as a list. Leave the key out. The whole document is at most 4 MB. The request stack trims every string and turns an empty string into null. A patch section value is the exception: an empty string there is refused, as Versions says. A workspace covers its projectRefs and every project whose logical key is in projectKeys, as Conn combines them. T3 derives a logical key from the git remote by default, so devices that keep T3’s default repository grouping agree on it.

Versions

The document has a version, and so has each section. A new profile starts at document version 1 with no workspaces. Every write adds 1 to the document version. A write also adds 1 to the version of each section it changed. A section with no recorded version is at version 0. This includes a section written before sections had versions. GET /profiles/{id}/settings returns sections: for each section that was written, its version, the time of the last write in updated_at, and the name of the Node that wrote it in updated_by. A section that is not listed is at version 0. There are two ways to write. Prefer PATCH. PATCH /profiles/{id}/settings changes the sections it holds, each with the version the client last read. It never touches a section it does not name. So a phone font change does not conflict with a Mac theme change. PUT /profiles/{id}/settings replaces the sections it holds and keeps the others. It checks the document version, as it always did. devices replaces every device type, and the types it leaves out are removed. It must hold at least one type. To remove the last type, patch it to null. A patch whose section is at another version is refused with conn.settings_version_conflict (409), and the Gateway writes nothing. details.current_version is the document version. details.sections holds the current version of each section the request named. The client reads again, merges the sections that changed, and retries. A value of null removes appearance or a device section. Its version still goes up, so an old client cannot write it back. workspaces cannot be null: send []. Only an explicit null removes a section. An empty or blank string is refused with validation.failed (422), and the error names the field, such as sections.appearance.value. Both run in one transaction on the locked profile row, so two writes from the same version cannot both win. A section name that Conn does not know, such as theme, devices, or devices.Phone, is refused with validation.failed. A rule error names its field with dots, such as sections.devices.iphone.value.values.baseFontSize.

Registration

A T3 server registers from the Node it runs on. A job on that host issues an admin session for the Gateway with the stock T3 CLI, so the server runs unmodified upstream T3:
The job sends that token as admin_session, with the server’s environment id, its label, and the URL that devices use to reach it over WireGuard.
  1. The Gateway reads GET <url>/.well-known/t3/environment. The served environmentId must equal environment_id, or the request fails with conn.environment_mismatch (422) before the session is sent.
  2. It reads GET <url>/api/auth/session with the session as its bearer token. A session T3 does not accept fails with conn.session_rejected (502).
  3. The session must carry access:read and access:write. t3 auth session issue grants both. A session without them fails with conn.admin_scope_missing (422).
  4. The Gateway stores the session encrypted, with the expiry T3 reports. T3 sessions last 30 days by default.
  5. It lists the server’s client sessions and revokes every earlier Orbit Gateway session. A failure here does not fail the registration. This is why the job must issue the session with the label Orbit Gateway.
Registering again replaces the stored session. The job registers on each server start and before the session expires. An environment whose session expired shows status: session_expired and cannot mint pairings until it registers again.

Pairing and revocation

A device asks the Gateway for a pairing link to one registered T3 server. The Gateway calls POST <url>/api/auth/pairing-token with its admin session, a unique label, such as phone via Orbit k3j9x0qa, and T3’s administrative scopes (see below). T3 gives the session paired from that link the same client label. The Gateway records the pairing: the Node, the server, the T3 link id, the label, and the link’s expiry. The device opens the returned pairing_url and pairs as it does today, but without a manual step. The link is one-time and short-lived, so the device pairs right away. To revoke, the Gateway lists GET <url>/api/auth/clients. It revokes each session whose label belongs to the Node’s open pairings on that server with POST <url>/api/auth/clients/revoke, and it revokes each pairing link that was not used with POST <url>/api/auth/pairing-links/revoke. It never revokes its own current session. The device loses that server at once; its WireGuard access does not change.

API

Every endpoint is under https://gateway.orbit/api/v1/conn and answers {"data": ..., "meta": {"request_id": ...}}. A failure answers {"error": {"code", "message", "details"}}. The generated API reference lists the same operations with their schemas. The objects:
pairing_url has T3’s form, <url origin>/pair#token=<token>. Every call is recorded as command activity with the calling Node. A registration records its environment id, label, and URL, never the admin session. A settings write records only section names and versions, because the document can hold workspace pictures, themes, and device values. A replace records the document version it replaced and the names of the sections it sent. A patch records each section name with the version the client sent.

Why it works this way

The T3 Connect relay could also be self-hosted. It needs four outside services and puts Cloudflare in the traffic path, and it has no place for workspaces. The Gateway already knows every machine and runs on the private network, so it adds no new service. The Gateway uses T3’s own admin sessions and pairing instead of a new T3 auth scope. The host issues the admin session with the stock t3 CLI, so servers run unmodified upstream T3, and the Gateway needs no SSH access to them. The cost is that the Gateway holds full admin access to every registered T3 server. On a private WireGuard network where the Gateway already manages every machine, that adds little new risk. A device pairing link carries T3’s administrative scopes, including access:read and access:write, which T3 lets an admin session grant. The servers are headless, so the device is the only place to manage a server’s linked environments and to answer an agent’s environment-link request. Each paired device therefore gets admin access to the server, the same trust the Gateway already holds. Sessions paired before this change keep their old scopes until the device pairs again. The Node is the identity because the Gateway already knows every WireGuard peer as a Node, phones included, and WireGuard binds each address to one key. Revocation happens only in T3 and leaves WireGuard access alone, so a revoked device keeps its other servers and Orbit access. A Node belongs to one profile because each device is its own Node: a profile is how a phone and a desktop share one copy of the workspaces. Settings use a version check rather than last-write-wins, so an edit on one device is never lost silently to an older copy on another.

Why sections have their own versions

The devices of a profile edit different things at different times. With one version for the whole document, a font change on the phone would conflict with a theme change on the Mac. Every client would then merge the entire document to resolve it. With a version per section, only two edits of the same section conflict. Each device type has its own section for the same reason: a phone and a desktop never conflict over values that belong to one of them. A patch is all or nothing, so a client that changes the mode and a theme in one step never leaves them half applied. The Gateway stores the versions apart from the document, in section_meta on the profile. A profile from before sections has no entry, so every section of it is at version 0, and the first patch of a section sends version 0. PUT stays for clients that only know workspaces. It replaces just the sections it sends, so such a client cannot erase the appearance or device settings that a newer client wrote.