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:

  1. Verification. Authorization: Bearer <prefix>… on any request resolves to an apiKey Subject carrying the key's tenantId + scopes — exactly like a session, so handlers, requireScope, and tenant scoping all just work. The strategy runs in the same auth chain as voltro dev and voltro serve.
  2. 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 (rotate revokes 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 stamps lastUsedAt.
  • Keys are tenant-scoped — a key carries its tenantId into every call.