Launch NowDocs

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

FileRole
lib/auth.tsapiKey() plugin configuration and the api_key_created notification hook.
lib/auth-client.tsapiKeyClient(), which adds authClient.apiKey.*.
app/(app)/account/keys/page.tsxThe /account/keys page.
features/account/api-keys/*Settings UI, dialogs and React Query hooks.
lib/account-actions.tssetApiKeyEnabled server action.
lib/config/site-config.tssiteConfig.slug, used as the key prefix.

The plugin is configured in lib/auth.ts:

lib/auth.ts
apiKey({
  defaultPrefix: `${siteConfig.slug}_`,
  requireName: true,
  keyExpiration: { minExpiresIn: 1, maxExpiresIn: 365 },
}),
  • defaultPrefix: every key starts with siteConfig.slug followed by an underscore. With the demo config that is pulse_. Rename the product in lib/config/site-config.ts and 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:

ActionHookCall
ListuseApiKeysauthClient.apiKey.list()
CreateuseCreateApiKeyauthClient.apiKey.create({ name, expiresIn })
RenameuseRenameApiKeyauthClient.apiKey.update({ keyId, name })
Enable or disableuseToggleApiKeyEnabledsetApiKeyEnabled server action (auth.api.updateApiKey)
DeleteuseDeleteApiKeyauthClient.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:

features/account/api-keys/create-api-key-dialog.tsx
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:

app/api/v1/resource/route.ts
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:

lib/auth.ts
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,
  },
}),
Changing rateLimit affects keys created afterwards. Existing keys keep the limit they were created with.

Customization

  • Change the prefix: change siteConfig.slug, or set defaultPrefix in lib/auth.ts directly.
  • Change the expiration choices: edit EXPIRATION_ITEMS in features/account/api-keys/types.ts and the expiresIn enum in create-api-key-dialog.tsx. Keep them within keyExpiration.
  • Change the usage hint: edit CURL_SNIPPET in features/account/api-keys/api-key-usage-hint.tsx to show your real API URL.

See the Better Auth API key plugin docs for the other options, such as permissions and usage quotas.

Next steps