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 config file you’d have to type by hand.
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 reads it back from
SUBSPACE_DEVICE_TOKENin its environment, the extension writeschrome.storage.local.deviceToken, mobile writessubspace.deviceTokenin AsyncStorage. Every subsequent request carries it asx-subspace-device-tokenon HTTP or?token=on a WebSocket connect, exactly 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 stored in SUBSPACE_DEVICE_TOKEN |
The Electron shell, including its terminal and PTY host. |
| Mobile | QR scan, token stored in AsyncStorage | 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 isn’t a blanket credential, what it can reach still 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, not instead of it: a paired device still resolves to
a Principal with the pairing user’s isAdmin flag and capabilities, and every one of
these domains checks that principal the same way it would check a session cookie.