Device pairing by QR code

The phone companion pairing payload a QR code carries, rendered on the token result page and by the token command, plus setting the public URL.

Pairing hands a phone companion everything it needs to reach and authenticate against this control plane in one scan: the base URL, a freshly minted access token, the certificate fingerprint to pin, the token's scope, and a device name. A QR carries that as a small JSON payload; the phone decodes it and connects, pinning the fingerprint trust-on-first-use on the first request.

Pairing is minting a token with a picture attached — it adds no new credential or trust path. The token in the payload is the same one-time secret token new prints; the fingerprint is the same one LAN access pins.

The payload

The QR encodes this JSON (schema version v, currently 1):

{
  "v": 1,
  "url": "https://exocomp.example.ts.net",
  "token": "exo_42_x3Y…",
  "fingerprint": "AB:CD:…",
  "scope": "operator",
  "name": "pixel-8"
}
Field Meaning
v Payload schema version; a scanner keys its parsing off it.
url Base URL the phone dials — the operator-set public URL, else the browser's own origin.
token The full one-time access token (exo_<id>_<secret>). Holding the QR is holding the token.
fingerprint Certificate SHA-256 to trust-on-first-use pin. Omitted only when it cannot be resolved (the phone still pins on first connect).
scope The token's scope: operator, readonly, or approve.
name The device name the token was minted under.

The payload is defined once in internal/pairing and rendered two ways: an inline SVG on the web token-result page and a half-block QR in the terminal.

From the browser

On the tokens page, the "Mint a token" action mints a token and shows the secret once, a copy control, and the pairing QR. Leave the token name blank to auto-generate a unique device-<hex> name; pick a scope (operator is the default); confirm with your own access token. A rotated token shows the QR too.

The QR's url is the operator-set public URL when configured, else the address the browser reached the page on, so pairing works before a public URL is set.

From the CLI

exocomp token new pixel-8 --scope operator

token new prints the token once, the fingerprint to pin, and — by default — a scannable terminal QR with the same payload (--qr=false to omit). The url comes from daemon.public_url; when unset the QR carries a clear placeholder and the output says to set it.

The public URL

url resolves from the daemon.public_url setting. Set it with exocomp serve --public-url <url>, or on the System page's Public URL control (operator-gated). When unset, the browser flow falls back to the request's own scheme and host; the CLI flow emits a placeholder.

Handling

The payload holds a live credential. It is never logged, persisted, or flashed through a redirect — it exists only on the operator's screen for the one render, the same discipline as the one-time secret it carries.

Related: LAN access · Running the daemon.