Public API & API Keys
Public API & API Keys
What it does
A versioned /api/v1 route group, authenticated via a Bearer API key
instead of the session cookie every other route uses. Users create/revoke
their own keys at /settings/api-keys; each key is scoped to that user (not
an organization) and per-key rate-limited by better-auth's own
@better-auth/api-key plugin.
Two routes ship as the worked example:
GET /api/v1/me— the authenticated user's profile.GET /api/v1/organizations— the organizations they're a member of, with their role in each.
How to configure it
Nothing to configure — the plugin is always on, no env var gate (unlike most
optional integrations in this starter, there's no external service here to
be unconfigured; the DB is the storage). lib/auth.ts's apiKey() call
sets a launchpad_ key prefix and a default per-key rate limit of 120
requests/hour, both adjustable there.
How it works
lib/auth.ts'sapiKey()plugin call (@better-auth/api-key— a separate package frombetter-authitself; this Next version's environment ships it split out, seeAGENTS.md).references: "user"(the default) scopes every key to a single user — no org-scoped keys.lib/db/schema/auth.ts'sapikeytable — hand-written to match the plugin's expected schema exactly, same@better-auth/cli generatecan't introspectlib/auth.tslimitation as every other plugin table in this starter (see organizations.md).keystores the hashed value; the plaintext is only ever returned once, at creation.app/(app)/settings/api-keys/page.tsx+components/settings/ api-keys-manager.tsx— lists the caller's own keys (auth.api.listApiKeys, session-scoped automatically via the plugin's ownsessionMiddleware) and lets them create (authClient.apiKey.create) / revoke (authClient.apiKey.delete) directly from the client, same pattern ascomponents/settings/linked-accounts.tsx— no server action needed, the plugin's own client methods are the RPC layer.lib/api-auth.ts'sverifyApiRequest(request)— the shared guard forapp/api/v1/*route handlers. Extracts theBearertoken fromAuthorization, callsauth.api.verifyApiKey({ body: { key } })directly rather than relying on the plugin's own request-mocking hook (enableSessionForAPIKeys, off by default and explicitly "not recommended for production" per the plugin itself) — that hook only intercepts requests that go through better-auth's own dispatcher (auth.api.*calls, the/api/auth/[...all]catch-all), not arbitrary custom route handlers like these. Maps a disabled/unknown/expired key to401, the plugin's own per-key rate limit being exceeded to429.app/api/v1/me/route.ts/app/api/v1/organizations/route.ts— callverifyApiRequest, then a plain Drizzle query scoped toauth.userId. These two are the worked examples to copy:mefor "something about the caller",organizationsfor "a list of resources the caller owns".
How to extend it
- A new
/api/v1/*route — callverifyApiRequest(request)first, return itserror/statuson!ok, otherwise useauth.userIdto scope your query. Same shape as the two existing routes. - Org-scoped keys instead of user-scoped — pass
references: "organization"toapiKey()inlib/auth.ts; you'll also needorganizationIdoncreateApiKeycalls and to updatecomponents/settings/api-keys-manager.tsx's create form. - Different rate limits per key —
createApiKey's body acceptsrateLimitMax/rateLimitTimeWindowper-key (server-only fields); expose them in the create form if you want users to choose. - Permissions/scopes — the plugin supports a
permissionsfield per key anddefaultPermissionsinapiKey()'s config; unused here (every key can hit every/api/v1/*route) but available if you need finer-grained access.
How to remove it
Drop the apiKey() plugin and its import from lib/auth.ts, apiKeyClient()
from lib/auth-client.ts, the apikey table from
lib/db/schema/auth.ts (regenerate a migration), lib/api-auth.ts,
app/api/v1/, app/(app)/settings/api-keys/,
components/settings/api-keys-manager.tsx, and the "API keys" sidebar link
in app/(app)/layout.tsx.