API Keys #
Summary #
Admins issue API keys in the admin area. Each key has a label and a set of roles, and is sent as Authorization: Bearer <key>. A route opens itself to API keys by naming the roles it allows, and only those routes accept keys. The first such route is cron, from [[cron]].
Motivation #
Some server work has to be started from outside the browser, such as a crontab calling the server on a schedule. Those callers have no atproto account and no web session, so they need a credential an admin can issue and revoke without touching the server's environment or restarting it.
Design #
Keys #
api_key (id text primary key, label text, key_hash text unique, roles_json text, created_by text, created_at text, last_used_at text null)
- Format. A key is
scn_followed by 32 random bytes in base64url. It is shown once, when it is issued, and only its SHA-256 hash is stored. A key is random, so a fast hash is enough, and a key is found by looking up its hash. - Roles. A key holds one or more existing roles, including the built-in
userandadmin. A key has no DID, so it holds only the roles it's given, and PDS host and handle domain matches don't apply to it. - Changing a key. Keys can't be edited. To change a key's roles, issue a new one and revoke the old.
- Deleting a role that a key holds is refused, with the keys listed, like the other uses of a role in [[admin]].
- Last used. A successful request with a key records the time, so an admin can spot keys nobody uses.
Route access #
requireApiKey(...roles) is Hono middleware that opens a route to API keys:
- No
Authorization: Bearerheader, or a key that doesn't match, is a 401. - A key that holds none of the route's roles is a 403.
- A route without
requireApiKeynever accepts a key, and a route with it never accepts a web session.
Keys never appear in logs. A rejected request logs the route and the reason, and an accepted one logs the key's label.
Admin API #
| Route | Purpose |
|---|---|
GET /api/admin/api-keys |
Every key's ID, label, roles, creator, created time, and last use. Never the key or its hash. |
POST /api/admin/api-keys |
Issue a key from { label, roles }. Answers with { id, key }, the only time the key is returned. |
DELETE /api/admin/api-keys/:id |
Revoke a key. |
Admin area #
API keys (/admin/api-keys): a table of keys with their label, roles, created time, last use, and a Revoke button, and a form with a label and a checkbox per role that issues a key and shows it once, with a note that it can't be shown again.
Documentation #
CLAUDE.mdanddocs/architecture.md: the rule against a public HTTP API allows routes opened to API keys, currently cron.docs/deployment.md: issuing a key for cron.
Scope Boundaries #
- No keys acting as a user. A key has no DID and can't read or write chats.
- No keys on existing routes. Only routes that name roles with
requireApiKeyaccept them. - No expiry. A key lasts until it is revoked.
- No keys in
.envor files. Keys are issued and revoked in the admin area, apart from the single-use keys thesite-configCLI issues itself and revokes, from [[site-config]].
Edge Cases and Decisions #
- Keys are compared by hash lookup, so there is no timing difference to measure between keys.
Acceptance Criteria #
Files #
apps/server/src/auth/api-keys.tsapps/server/src/admin/routes.tsapps/server/src/db/migrations/apps/web/src/features/admin/,apps/web/src/routes/_app/admin/CLAUDE.md,docs/architecture.md,docs/deployment.md