Device pairing
Anything that isn’t a browser tab with a session cookie, the desktop app, the mobile app, the Chrome extension, or a future headless client, authenticates with a device token instead. Pairing is how that token gets from the server to the client without ever putting a long-lived credential in a QR code or a hand-typed config file.
Pairing a device
Section titled “Pairing a device”-
Mint a code
From a signed-in web session, Settings → Devices → “pair a new device” calls the
devices.pairStarttRPC procedure. It mints a one-shot 8-character code drawn from an unambiguous alphabet (no0/O,1/Iconfusion), stores only its sha256 inops.pairing_codes, and sets a 5-minute TTL. Mobile pairing shows the same code as a QR so you don’t type it on a phone keyboard. -
Redeem it
The new client posts to the open route
POST /pair/claim {code, name, kind}. On a valid, unexpired, unclaimed code (matched case-insensitively), the server issues a device token and returns it in the response body, once. Any capability flags attached to the pairing code (see below) carry onto the new device row. -
Store it
The client persists the token: desktop encrypts it with Electron
safeStoragein a main-process-only0600file, the extension writes its extension-origin-isolatedchrome.storage.local.deviceToken, and mobile stores it in the OS keychain through Expo SecureStore. Every subsequent request carries it asx-subspace-device-tokenon HTTP. For WebSockets it exchanges the token for a one-use?ticket=credential atPOST /auth/socket-ticket, as described in identity & auth.
Which clients pair
Section titled “Which clients pair”| Client | Pairs via | Typical use |
|---|---|---|
| Web | N/A, uses session cookies, not device tokens | The client that mints pairing codes for everything else. |
| Desktop | Code entry during first-run setup, token encrypted outside the renderer | The Electron shell, including its terminal and PTY host. |
| Mobile | Code entry, token stored in the OS keychain | The Expo app, reachable over Tailscale. |
| Extension | Code entry in the popup, token stored in chrome.storage.local |
The Chrome extension’s capture and automation host. |
The device list and revocation
Section titled “The device list and revocation”devices.list returns your own devices; an admin’s call returns every device in the
instance, each with its kind, last-seen timestamp, and revoked status. devices.revoke
is owner-or-admin, so you can kill a lost phone yourself without waiting on an admin, and
an admin can kill anyone’s device in an incident.
Revoking a device does three things in one step:
- Stamps
ops.devices.revoked, so the token immediately failsresolveDeviceTokenon the device’s very next request (a fresh 401). - Deletes any web sessions tied to that specific device row, in case a “device” is actually a browser session that shared the row.
- Terminates that device’s live
/wsconnections by pushing adevice-revokedoutbox notice, closing the socket server-side rather than waiting for the client to notice its token stopped working.
Trust domains per device kind
Section titled “Trust domains per device kind”A device token is not a blanket credential; what it can reach depends on the kind of access behind it:
| Domain | Trust level | Why |
|---|---|---|
| Disk (the OKF mirror) | Operator | Filesystem access to the exported graph is equivalent to operating the machine the server runs on, not just using the app. |
| Comms (mail, calendar, IM) | Personal, scoped to one user | Covered in full on per-user agents & comms; no device, paired or not, reaches another user’s inbox. |
PTY, os.exec, code.* dispatch |
Admin-only | A host shell is the highest tier of access in the system; only an admin principal reaches it, paired or via a browser session. |
These rules apply on top of pairing: a paired device still resolves to a Principal with
the pairing user’s isAdmin flag and capabilities, and each domain checks that principal
the same way it would a session cookie.