# 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 (``, `