API keys
Personal API keys with a product prefix, expiration, rename, disable and delete, built on the Better Auth API key plugin, and how to verify them.
Users can create personal API keys from Account → API Keys to call your API from scripts and other
services. Launch Now uses the @better-auth/api-key plugin: keys are hashed in the database, shown once
at creation, and start with your product's prefix, for example pulse_.
How it works
| File | Role |
|---|---|
lib/auth.ts | apiKey() plugin configuration and the api_key_created notification hook. |
lib/auth-client.ts | apiKeyClient(), which adds authClient.apiKey.*. |
app/(app)/account/keys/page.tsx | The /account/keys page. |
features/account/api-keys/* | Settings UI, dialogs and React Query hooks. |
lib/account-actions.ts | setApiKeyEnabled server action. |
lib/config/site-config.ts | siteConfig.slug, used as the key prefix. |
The plugin is configured in lib/auth.ts:
apiKey({
defaultPrefix: `${siteConfig.slug}_`,
requireName: true,
keyExpiration: { minExpiresIn: 1, maxExpiresIn: 365 },
}),defaultPrefix: every key starts withsiteConfig.slugfollowed by an underscore. With the demo config that ispulse_. Rename the product inlib/config/site-config.tsand new keys get the new prefix; existing keys keep theirs.requireName: true: a key can't be created without a name.keyExpiration: the expiration a client can request is between 1 and 365 days.
Everything else uses the plugin's defaults, including the header name (x-api-key) and per-key rate
limiting (see Rate limits).
Managing keys
The page at /account/keys renders ApiKeysSettings (features/account/api-keys/api-keys-settings.tsx).
The hooks in use-api-keys.ts wrap the client with React Query:
| Action | Hook | Call |
|---|---|---|
| List | useApiKeys | authClient.apiKey.list() |
| Create | useCreateApiKey | authClient.apiKey.create({ name, expiresIn }) |
| Rename | useRenameApiKey | authClient.apiKey.update({ keyId, name }) |
| Enable or disable | useToggleApiKeyEnabled | setApiKeyEnabled server action (auth.api.updateApiKey) |
| Delete | useDeleteApiKey | authClient.apiKey.delete({ keyId }) |
The create dialog asks for a name (up to 32 characters) and an expiration of 7, 30 or 90 days, or none. The value is converted to seconds before it is sent:
createMutation.mutate({
name: values.name,
expiresIn:
values.expiresIn === "never"
? undefined
: Number(values.expiresIn) * 24 * 60 * 60,
})After creation, ApiKeyCreatedDialog shows the full key once. The list only shows the first characters
(start) and a status badge: Active, Disabled or Expired. To rotate a key, delete it and
create a new one.
Creating a key sends the user a security.api_key_created notification, from the after hook in
lib/auth.ts. The shared demo account can create keys but can't delete them.
Verifying a key
The boilerplate doesn't ship an API route that accepts keys. The usage hint on the keys page shows the
expected format, with the key in the x-api-key header:
curl -X GET "https://your-domain.com/api/v1/resource" \
-H "x-api-key: YOUR_API_KEY"To accept keys in your own route handler, verify them on the server with auth.api.verifyApiKey. It
returns valid, an error with a code, and the key record without the secret. The key's
referenceId is the ID of the user who created it:
import { auth } from "@/lib/auth"
import { NextResponse } from "next/server"
export async function GET(request: Request) {
const key = request.headers.get("x-api-key")
if (!key) {
return NextResponse.json({ error: "Missing API key" }, { status: 401 })
}
const result = await auth.api.verifyApiKey({ body: { key } })
if (!result.valid || !result.key) {
return NextResponse.json(
{ error: result.error?.message ?? "Invalid API key" },
{ status: 401 }
)
}
const userId = result.key.referenceId
// ... load and return the user's data
return NextResponse.json({ userId })
}Verification fails when the key doesn't exist, is disabled, has expired or has hit its rate limit. A successful verification updates the key's last request time, shown in the keys list.
Good to know: verifyApiKey checks the key only. Scope every query by the returned user ID, and
check the user's plan with getUserPlan() if API access depends on it. See
Billing.
Rate limits
The plugin rate-limits each key by default. lib/auth.ts doesn't override it, so each key allows 10
requests per day. The limit is stored on the key when it is created. Before you ship a public API, raise
it in the plugin configuration:
apiKey({
defaultPrefix: `${siteConfig.slug}_`,
requireName: true,
keyExpiration: { minExpiresIn: 1, maxExpiresIn: 365 },
rateLimit: {
enabled: true,
timeWindow: 1000 * 60 * 60 * 24, // 1 day, in milliseconds
maxRequests: 10_000,
},
}),rateLimit affects keys created afterwards. Existing keys keep the limit they were created with.Customization
- Change the prefix: change
siteConfig.slug, or setdefaultPrefixinlib/auth.tsdirectly. - Change the expiration choices: edit
EXPIRATION_ITEMSinfeatures/account/api-keys/types.tsand theexpiresInenum increate-api-key-dialog.tsx. Keep them withinkeyExpiration. - Change the usage hint: edit
CURL_SNIPPETinfeatures/account/api-keys/api-key-usage-hint.tsxto show your real API URL.
See the Better Auth API key plugin docs for the other options, such as permissions and usage quotas.