Skip to main content
Localizia

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/revoke reject 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.