zazzy
Features

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 & storagegenerateApiKey() (lib/keys.ts) produces an sk_live_… token (~192 bits). Only the SHA-256 hashApiKey() digest (keyHash) is persisted, plus a non-secret keyPrefix display handle; the plaintext is returned once and never stored again.
  • Scopeslib/scopes.ts defines API_KEY_SCOPES = ["read", "write"] and hasScope(). The grammar is resource:action; v1 is coarse (write implies read, D-054) and fail-closed on anything unparseable.
  • Management routercreateApiKeysRouter({ audit? }) mounts as apiKeys; create / list / revoke run on orgProcedure.role("admin"). list returns a view that never exposes keyHash or plaintext; revoke idempotently stamps revokedAt.
  • Bearer authauthenticateApiKey(db, req) looks a token up by its unique keyHash, rejecting missing/unknown/revoked/expired with one generic 401, and throttles lastUsedAt writes to once a minute (D-055).
  • Route wrapperwithApiAuth(handler, { scope, … }) runs auth → scope check (403) → rate limit (429 with X-RateLimit-* / Retry-After) → attribution → handler, behind a single JSON error envelope.
  • Rate limitingPostgresRateLimiter wraps rate-limiter-flexible's Prisma store over the ApiRateLimit model in a fixed 60s window; DEFAULT_API_RATE_LIMIT_PER_MINUTE = 60, with -1 meaning unlimited.
  • Settings UI & endpointsApiKeysManager (create dialog with one-time reveal, per-row revoke) at /settings/api-keys; the served routes are GET /api/v1/me and GET /api/v1/organization.

Models it adds

ModelPurpose
ApiKeyAn org-scoped public-API credential: keyHash (unique), keyPrefix, scopes, and optional lastUsedAt / expiresAt / revokedAt. createdById is a plain audit column, not a User relation.
ApiRateLimitThe 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 is orgProcedure.role("admin") scoped to the active org, ApiKey cascades from Organization, and the /api/v1/organization route 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 the apiAccess entitlement gate; usage records apiRequests; 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.

On this page