API Keys
Prov0.1.0Let your users create scoped API keys for your API, and verify them in your route handlers with one call: hashed storage, rotation with a grace period, revocation, expiry, per-key rate limits and daily usage.
terminalnpx ajolla add api-keysWhat you get
- Keys like sk_live_… shown once, stored only as a SHA-256 hash with a 4-character preview
- Scopes from a catalog you define, with wildcards (forms.*) and admin-only scopes
- verifyApiKey(request, { scopes }) for route handlers: timing-safe, checks expiry, revocation and scopes
- Rotate with a grace period, revoke instantly, optional expiry with an email warning before it lapses
- Optional per-key rate limits (database fixed window, or plug in your own limiter)
- Last-used time and IP (throttled writes) and sampled per-day usage per key
- Admin screen: every key, revoke any, edit the scope catalog, usage per key
- Personal or organization-owned keys when Roles & Permissions is installed
Routes it adds
/account/api-keys/account/api-keys/[keyId]/admin/api-keys/admin/api-keys/[keyId]/admin/api-keys/scopes
/api/ajolla/api-keys/maintenance/api/ajolla/api-keys/whoami
Plus 1 Supabase migration with row-level security, and an AGENTS.md your coding agent reads after install.
Installs with
Kernel (added automatically).
Configuration
- API_KEYS_PREFIX
- First segment of new keys (default "sk" → sk_live_…). 1–12 lowercase letters/digits.
- API_KEYS_ENVIRONMENTS
- Comma-separated environments users can pick (default "live,test"); the first is the default.
- API_KEYS_ROTATION_GRACE_HOURS
- How long a rotated key keeps working (default 24; 0 revokes it immediately).
- API_KEYS_MAX_PER_OWNER
- Active keys allowed per user or organization (default 25).
- API_KEYS_MAX_LIFETIME_DAYS
- If set, every key must expire within this many days.
- API_KEYS_RATE_LIMIT_PER_MINUTE
- Default per-key limit in requests per minute (default 0 = off). Admins can override per key.
- API_KEYS_USAGE_SAMPLE_RATE
- Fraction of requests written to the usage log, 0.01–1 (default 1). Lower it for high-volume APIs.
- API_KEYS_EXPIRY_WARNING_DAYS
- Warn owners this many days before a key expires (default 7).
- NEXT_PUBLIC_SITE_URL
- Absolute URL of your app; makes links in security notifications absolute.
Events
Other modules (and your own code) can react to these:
api_keys.createdapi_keys.revokedapi_keys.rotatednotifications.requested
Use it from any stack
Ajolla Hosted runs the same code as a multi-tenant API. Create an app in your dashboard, enable API Keys, and call /api/m/api-keys/v1 with your app key.
OpenAPI document — feed it to Ajolla to generate SDKs in 7 languages and an MCP server.
AGENTS.md
Read the instructions installed with this module
markdown# Ajolla API Keys
This module lets your users create **API keys for your own API**, and gives your route handlers one call to verify them. Keys look like `sk_live_4fT9…Qz1a`, are shown once, and are stored only as a SHA-256 hash plus their last 4 characters. "Admins" means users with the `api_keys.manage` permission.
## What was installed
**Routes**
- `/account/api-keys`: a signed-in user's keys. Create a key (name, environment, permissions from your scope catalog, expiry), copy it once, rotate or revoke. `?org=<orgId>` shows an organization's keys to users holding `api_keys.manage` in that org (Roles & Permissions).
- `/account/api-keys/[keyId]`: details, 30-day usage, rename, rotate, revoke.
- `/admin/api-keys`: every key with status, owner, scopes and requests in the last 30 days; filter by status, name or owner; revoke any key (the owner is notified).
- `/admin/api-keys/[keyId]`: details, usage, per-key rate limit, revoke with a reason.
- `/admin/api-keys/scopes`: the scope catalog. Add, describe, mark admin-only, delete.
- `GET /api/ajolla/api-keys/whoami`: an example key-protected route that returns the key's principal.
- `GET /api/ajolla/api-keys/maintenance`: the daily cron (expiry warnings and pruning).
**Code**
- `lib/ajolla/api-keys/core.ts`: all logic, framework-free (also used by Ajolla Hosted).
- `lib/ajolla/api-keys/token.ts`: key format, hashing, constant-time compare, scope matching.
- `lib/ajolla/api-keys/server.ts`: `verifyApiKey`, `apiKeyErrorResponse`, `requireApiKey`, config from env.
- `lib/ajolla/api-keys/view.ts`: labels and types shared with client components.
- `components/ajolla/api-keys/`: create form, show-once key reveal, action buttons, usage chart.
- `lib/ajolla/admin/api-keys.ts`: the admin page registration.
**Tables** (`ajolla_api_keys_`)
- `scopes`: the catalog (`name` unique; `admin_only`).
- `keys`: `token_hash` (unique, indexed — the verification lookup), `preview`, `scopes text[]`, `expires_at`, `revoked_*`, `rotated_from`/`replaced_by`, `rate_limit_per_minute`, `last_used_at`/`last_used_ip`, optional `org_id`.
- `usage`: requests per key per UTC day.
- `rate_windows`: one-minute counters for rate-limited keys.
**RLS**
- Users read the catalog; admins manage it.
- Users see and update their own keys; org managers see their org's keys; admins see all. Only admins delete keys.
- A guard trigger rejects scopes outside the catalog (and admin-only scopes from non-admins), and stops users from changing a key's hash, owner, scopes, limits or bookkeeping, un-revoking it or extending its expiry.
- Usage is readable wherever its key is. Usage and rate windows are written only by server code using the secret key.
- Anonymous visitors see nothing.
**Events**
- `api_keys.created` `{ keyId, userId, orgId, name, environment, scopes, expiresAt }`
- `api_keys.revoked` `{ keyId, userId, orgId, revokedBy, reason }`
- `api_keys.rotated` `{ keyId, previousKeyId, userId, orgId, previousExpiresAt }`
- `notifications.requested` (category `security`): when a key is created, when an admin revokes someone's key, and when a key expires within the warning window. Install Notifications to deliver them; without it nothing breaks.
**Env** (all optional): `API_KEYS_PREFIX`, `API_KEYS_ENVIRONMENTS`, `API_KEYS_ROTATION_GRACE_HOURS`, `API_KEYS_MAX_PER_OWNER`, `API_KEYS_MAX_LIFETIME_DAYS`, `API_KEYS_RATE_LIMIT_PER_MINUTE`, `API_KEYS_USAGE_SAMPLE_RATE`, `API_KEYS_EXPIRY_WARNING_DAYS`, `NEXT_PUBLIC_SITE_URL`. See `.env.example`.
## Setup
1. Apply the migration with `supabase db push`.
2. Define scopes at `/admin/api-keys/scopes` (e.g. `forms.read`, `forms.write`, `forms.*`).
3. Link to `/account/api-keys` from your account menu.
4. Schedule the maintenance cron in `vercel.json`: `{ "crons": [{ "path": "/api/ajolla/api-keys/maintenance", "schedule": "0 8 * * *" }] }` (uses the kernel's `CRON_SECRET`).
5. Protect your routes:
```ts
// app/api/forms/route.ts
import { apiKeyErrorResponse, verifyApiKey } from "@/lib/ajolla/api-keys/server";
export async function GET(request: Request) {
const auth = await verifyApiKey(request, { scopes: ["forms.read"] });
if (!auth.ok) return apiKeyErrorResponse(auth); // 401 / 403 / 429 with { error: { code, message } }
const { userId, orgId, scopes } = auth.principal;
// … query as that user (with the secret-key client, filtered by userId) …
}
```
Clients send the key as `Authorization: Bearer <key>` or `X-API-Key: <key>`.
## How verification works
- Malformed keys are rejected without touching the database.
- One indexed lookup by `token_hash`, then a constant-time comparison of the hash.
- Revoked → `revoked_key`, expired → `expired_key` (401). Missing scopes → `insufficient_scope` (403, with `missingScopes`).
- Scope wildcards: a key with `forms.*` covers `forms.read` and `forms.submissions.export`; `*` covers everything. A route requiring `forms.*` needs `forms.*` or `*` on the key.
- Rate limit (only when the key has a limit, or `API_KEYS_RATE_LIMIT_PER_MINUTE` is set): fixed one-minute window → `rate_limited` (429 with `Retry-After`). Pass `rateLimiter` to `verifyApiKey` to use Redis/Upstash instead of the table.
- Bookkeeping runs after the response (`after()`): `last_used_at`/IP at most once a minute per key; usage counted per day, sampled at `API_KEYS_USAGE_SAMPLE_RATE` and scaled back up.
`verifyApiKey` uses the secret-key client because API requests carry a key, not a Supabase session. Query your own data for `principal.userId` accordingly.
## Rotation and revocation
- **Rotate** creates a replacement with the same name, scopes, environment, owner and lifetime. The old key keeps working for `API_KEYS_ROTATION_GRACE_HOURS` (default 24), then expires. With 0 it is revoked at once.
- **Revoke** is immediate and permanent.
- **Deleting a scope** from the catalog doesn't remove it from existing keys (nothing breaks mid-flight); such keys can't be rotated — create new ones.
## Limits
- 25 active keys per user or organization (`API_KEYS_MAX_PER_OWNER`); 50 scopes per key.
- Usage rows are kept 90 days; rate windows 10 minutes (pruned by the cron).
## Customising
- **Key format:** `API_KEYS_PREFIX` / `API_KEYS_ENVIRONMENTS`; the layout lives in `token.ts`.
- **Expiry choices** in the create form: `EXPIRY_CHOICES` in `view.ts`.
- **Notification wording:** `createdEvents`, `revokeKey` and `claimExpiringKeys` in `core.ts`.
- **Styles:** `components/ajolla/api-keys/api-keys.module.css`, plus the kit's `--aj-*` variables.