API keys
Overview
API keys let users (and their scripts, CI, or integrations) call your API without a browser session. The kit ships a native implementation — no third-party plugin — that fits its patterns: a Prisma model, tRPC procedures, and a verification helper wired into the request context.
Enable it with a feature flag:
AUTH_API_KEYS_ENABLED=true
When off, the endpoints and the account-settings UI are hidden.
How keys work
- A key is a high-entropy random token prefixed with
sk_, e.g.sk_a1b2c3d4e5…. - Only the SHA-256 hash is stored. The full key is shown once, at creation. Because the token is random and unique, a fast unsalted hash is the correct choice here (the same approach GitHub and Stripe use) — verification is an indexed hash lookup, with no plaintext comparison.
- Keys are revocable (soft
revokedAt), can expire, carry coarse scopes (read/write), and can be scoped to one organization.
Users manage their keys under account → security.
Authenticating a request
Send the key in the x-api-key header:
curl https://your-app.com/api/trpc/user.me \
-H "x-api-key: sk_a1b2c3d4e5..."
The tRPC context resolves the key to its owning user before the request runs, so
every protectedProcedure works unchanged whether the caller used a cookie
session or an API key. An org-scoped key also sets the active organization, so
protectedOrganizationProcedure resolves too.
Security model
- The full key never persists and is never returned again after creation.
- A revoked, expired, or banned-owner key resolves to "no auth".
- Key management requires a real session. A leaked key cannot mint or revoke
other keys —
create/revokereject requests authenticated via an API key (privilege-escalation guard). - Keys inherit the kit's per-identity tRPC rate limiting.
Enforcing scopes (optional)
Scopes are stored on each key and exposed to procedures as ctx.apiKey.scopes
(null for cookie sessions). Enforcement is opt-in — check the scope where it
matters:
if (ctx.apiKey && !ctx.apiKey.scopes.includes("write")) {
throw new TRPCError({ code: "FORBIDDEN", message: "errors.forbidden" })
}
Extending
The model lives in prisma/schema.prisma (ApiKey), the generate/verify
helpers in src/lib/api-keys/, and the procedures in
src/server/routers/api-keys.router.ts. To add finer-grained scopes, extend the
apiKeyScopes list in api-keys.schema.ts.