--- id: headscale/device-invites-platform-side title: "Device invites, platform side" theme: headscale status: done created: 2026-08-05 date_source: stated order: 5 --- **Device invites, platform side** (`COMMS/OFFSCALE_INVITE_ENROLLMENT.md` §5, built 2026-08-05). Authorize-new-device form, the one-time link with copy/share/QR, and the invite list with revoke — `/_officer/enroll/invites` in `invites.ts`, `InvitesView`. **The records are not Officer's.** They proxy to the server's Officer Companion, because the joining phone has to claim without an Officer account and this sidecar is loopback-only behind our auth; the spec's own "an invite must work when the platform is down" argument says the same. Officer stores no invite and no claim token. **Live on all four servers since 2026-08-05.** The 401 that blocked it was a companion-side prefix parse (a Headscale key prefix is a fixed 12 chars and may contain `-`; they split on the first one); fixed upstream. Verified end to end against pastilhas-eu: create returns the `officer-offscale://join#…` link, list shows the record, revoke flips it to `revoked`. The list envelope is not in the spec, so `pickInvites` takes the body's array whatever it is keyed under. Four gaps were sent back to the spec author: `keys.ts` has no sub-day key TTL for the 5-minute claim key, "the sidecar must refuse plaintext" is unenforceable behind nginx, `tailnet` is not a headscale concept and has to be recorded on the invite, and `/api/v1/enroll/*` collides with headscale's own namespace.