diff --git a/docs/mobile-api-keys.md b/docs/mobile-api-keys.md new file mode 100644 index 00000000..e089d52e --- /dev/null +++ b/docs/mobile-api-keys.md @@ -0,0 +1,261 @@ +# API keys — implementing the client side + +Written 2026-08-08, for the mobile developer. Server side is live and verified; nothing below is +planned-but-missing unless it says so. + +**The mobile apps are not built here.** This document is the boundary: what the server now accepts, what +changes in `monorepo-mobile`, and what does not. + +--- + +## The short version + +**An API key goes exactly where the JWT goes.** Same `Authorization: Bearer …` header, same `?token=` +query fallback for media, same `?token=` on the WebSocket. Every door was taught the new format at once, +so there is no endpoint where a key works and another where it doesn't. + +That is the whole point of the design, and it means the client change is small: + +- `packages/core/src/services/api.ts` needs **no change at all** — it already sends whatever + `getToken()` returns as a bearer token. +- `packages/core/src/state/useAuth.ts` gains a second way to obtain that value. +- Multi-server storage already exists (`tokenKeyFor(activeServerId())` in `services/servers.ts`), so a + key per server slots into the same SecureStore entry the JWT uses today. + +The problem being solved is **multiple logins across the apps**. Today every app signs in with the +password and gets its own 30-day JWT, and there is no way to cut one device off without a password +change that kills all of them. A key is revocable on its own, from the web UI, in one click. + +--- + +## What a key looks like + +``` +ofk_eRgp_Fgqr5hKAyydvxfvu-HL12s-HYwSlTS7wYg-Ua0 +``` + +`ofk_` prefix, then 32 CSPRNG bytes base64url-encoded. **Treat it as opaque.** Do not parse it, do not +validate its length, do not assume 47 characters — the only guarantee is the `ofk_` prefix and that it is +URL-safe and header-safe. + +- **It does not expire** unless one was asked for at creation. Assume no expiry. +- **It is shown exactly once**, by the response that creates it. The server stores a SHA-256; there is no + endpoint that reads a key back and there never will be. If it is lost, revoke and mint another. +- **It carries the account's full authority** — the same as the password, no more. A member's key is + still a member. + +--- + +## Two ways for the app to get one + +### (a) Mint it in-app — recommended + +Sign in with the password once, immediately trade the JWT for a key, store the key, throw the JWT away. +No copy-paste, no typing 47 characters on a phone keyboard. + +``` +POST /api/auth/signin {email, password} → {token, user} +POST /api/api-keys {name: "iPhone 15"} → {entry, key} (Authorization: Bearer ) + store `key`, discard `token` +``` + +Name it after the device, not the app — the owner reads that string in the web UI when deciding what to +revoke, and "iPhone 15" is a decision they can make while "music" is not. + +If you want one key per app rather than per device, name it `""`. Either is fine; be +consistent so the list stays readable. + +### (b) Paste a key the owner made in the web UI + +**Settings → Integrations → Personal → API keys.** Useful for a device that cannot show a sign-in form, +and as the recovery path when (a) fails. A paste field that accepts the key and skips signin entirely. + +Validate only that it is non-empty and starts with `ofk_`, then make a real request and let the server +answer. + +--- + +## Using it + +Identical to the JWT in all three places: + +| Where | How | +|---|---| +| Normal requests | `Authorization: Bearer ofk_…` | +| Media URLs (``, `