API keys
First-class API keys — built into the framework. Bearer-token auth for headless callers + admin-gated issue/list/revoke management, with hash-only storage.
API keys are a first-class framework feature, not a plugin — the _voltro_api_keys table is framework-internal, and a single apiKeys: true in app.config.ts turns on both verification and management. Only the SHA-256 hash of a key is ever stored; the raw token is shown once at issue time and is unrecoverable thereafter.
Enable
// app.config.ts
export default {
type: 'api' as const, name: 'api',
apiKeys: true, // or { prefix?: 'myapp_', managementPath?: '/v1/api-keys' }
}This does two things:
- Verification.
Authorization: Bearer <prefix>…on any request resolves to anapiKeySubject carrying the key'stenantId+scopes— exactly like a session, so handlers,requireScope, and tenant scoping all just work. The strategy runs in the same auth chain asvoltro devandvoltro serve. - Management. Admin-gated REST routes mount under
managementPath(default/v1/api-keys):
| Method + path | Action |
|---|---|
POST /v1/api-keys/issue |
mint a key — returns the raw token once |
GET /v1/api-keys |
list the tenant's keys (no secrets) |
POST /v1/api-keys/revoke |
revoke a key by id |
All three require admin:full (ADMIN_SCOPE), so only an admin Subject can manage keys.
# An admin issues a key for a CI pipeline:
curl -XPOST https://api.example.com/v1/api-keys/issue \
-H 'Authorization: Bearer <admin-session>' \
-d '{"name":"CI deploy","scopes":["deploy:write"],"expiresInDays":90}'
# → { "id": "apikey_…", "token": "voltro_…", "keyPrefix": "voltro_ab12" } ← copy the token now
# The CI pipeline then authenticates with it:
curl https://api.example.com/... -H 'Authorization: Bearer voltro_…'In-handler service
Build your own management UI on the same ApiKeyService (issue / verify / rotate / revoke / list):
import { makeApiKeyService, dataStoreApiKeyStore } from '@voltro/runtime'
const svc = makeApiKeyService(dataStoreApiKeyStore(ctx.store))
const issued = await svc.issue({ tenantId, name: 'mobile app', scopes: ['read'] })
// show issued.token ONCE; later: svc.rotate(id), svc.revoke(id), svc.list(tenantId)Security model
- Hash-only storage. A DB dump never exposes a usable key — only
sha256(token). Lose a token → rotate it (rotaterevokes the old + issues a fresh one with the same scopes). - Scopes gate what a key can do (
requireScope(ctx.subject, 'deploy:write')); expiry (expiresInDays) and revoke stop it. Verification checks not-revoked + not-expired and stampslastUsedAt. - Keys are tenant-scoped — a key carries its
tenantIdinto every call.