API keys + public API
Programmatic access with scopes, attribution, and rate limits.
API keys give an org programmatic access to the public API under /api/v1. Keys
are minted per org, carry coarse read/write scopes, are stored only as a
SHA-256 hash (the plaintext is shown exactly once), and every public request is
authenticated, scope-checked, rate-limited, and attributed back to the key that
made it.
Optional feature
API keys is optional (core: false, requires: ["orgs"]). Keys and their
requests are org-scoped; the management UI is admin-only.
What it does
- Minting & storage —
generateApiKey()(lib/keys.ts) produces ansk_live_…token (~192 bits). Only the SHA-256hashApiKey()digest (keyHash) is persisted, plus a non-secretkeyPrefixdisplay handle; the plaintext is returned once and never stored again. - Scopes —
lib/scopes.tsdefinesAPI_KEY_SCOPES = ["read", "write"]andhasScope(). The grammar isresource:action; v1 is coarse (writeimpliesread, D-054) and fail-closed on anything unparseable. - Management router —
createApiKeysRouter({ audit? })mounts asapiKeys;create/list/revokerun onorgProcedure.role("admin").listreturns a view that never exposeskeyHashor plaintext;revokeidempotently stampsrevokedAt. - Bearer auth —
authenticateApiKey(db, req)looks a token up by its uniquekeyHash, rejecting missing/unknown/revoked/expired with one generic 401, and throttleslastUsedAtwrites to once a minute (D-055). - Route wrapper —
withApiAuth(handler, { scope, … })runs auth → scope check (403) → rate limit (429 withX-RateLimit-*/Retry-After) → attribution → handler, behind a single JSON error envelope. - Rate limiting —
PostgresRateLimiterwrapsrate-limiter-flexible's Prisma store over theApiRateLimitmodel in a fixed 60s window;DEFAULT_API_RATE_LIMIT_PER_MINUTE = 60, with-1meaning unlimited. - Settings UI & endpoints —
ApiKeysManager(create dialog with one-time reveal, per-row revoke) at/settings/api-keys; the served routes areGET /api/v1/meandGET /api/v1/organization.
Models it adds
| Model | Purpose |
|---|---|
ApiKey | An org-scoped public-API credential: keyHash (unique), keyPrefix, scopes, and optional lastUsedAt / expiresAt / revokedAt. createdById is a plain audit column, not a User relation. |
ApiRateLimit | The rate-limiter-flexible Prisma store (key, points, expire); column names are fixed by the library. |
Adds the marker-wrapped Organization.apiKeys relation, plus the nested
@feature:api-keys attribution columns UsageEvent.apiKeyId and
AuditLog.apiKeyId (plain nullable columns on models owned by usage and
audit-log, not relations).
Env it needs
None. Adds the rate-limiter-flexible dependency; the rate-limit store is a
Prisma model on the existing Postgres, so there is no new env var, script, or
compose service.
Composition
requires: ["orgs"]— every management procedure isorgProcedure.role("admin")scoped to the active org,ApiKeycascades fromOrganization, and the/api/v1/organizationroute reads org settings through the shared surface.- Soft consumer seams (injected via markers, api-keys never imports them):
audit-log records
api_key.created/api_key.revoked; flags supplies the per-key rate limit (getEntitlements(...).quotas.apiRequestsPerMinute) and theapiAccessentitlement gate; usage recordsapiRequests; jobs schedules the rate-limit cleanup cron. Each falls back cleanly when its feature is pruned. - Dependents — none; no feature requires api-keys.
What pruning removes
Leaving api-keys out drops features/api-keys, the entire /api/v1 route tree
(me, organization, api-route.ts), the /settings/api-keys page, the
apiKeys router mount, the ApiKey and ApiRateLimit models, the
Organization.apiKeys relation, and the apiKeyId attribution columns on
UsageEvent and AuditLog — plus the api-keys-rate-limit-cleanup cron and the
rate-limiter-flexible dependency. No feature requires api-keys, so removing it
forces nothing else out; the marked seams in flags, usage, and audit-log simply
drop their api-keys blocks.