BTST
PluginsQuickstartDocs
Live Blog

From research to product evaluation

Evaluating a publishing workflow for an app you already own?

See what the BTST Blog plugin adds to an existing React or Next.js app
BTST

Open-source TypeScript features for the React application, data, and deployment you already own.

Released plugins

  • Blog
  • AI Chat
  • CMS
  • Form Builder
  • UI Builder
  • Kanban
  • Comments
  • Media
  • Route Docs
  • OpenAPI
  • Better Auth UI

Resources

  • Quickstart
  • Documentation
  • All plugins
  • Live Blog
  • GitHub (opens in a new tab)
© 2026 BTST. Open source under the MIT License.
AI Chat
September 21, 2026ReactBetter Auth UI

Manage Personal API Keys with BTST Better Auth UI

Connect API key creation, verification, expiration, and revocation while enforcing ownership at the consuming endpoint.

Manage Personal API Keys with BTST Better Auth UI

An API key management page lets a signed-in user create and revoke credentials for scripts and integrations. The page does not protect an application endpoint by itself: that endpoint must verify the supplied key and authorize the requested operation.

This guide adds personal API keys to an existing BTST auth integration using @btst/stack@3.1.2, @btst/better-auth-ui@2.0.1, and better-auth@1.6.16 with @better-auth/api-key@1.6.16. It covers user-owned keys with the default configuration. Organization-owned keys require a separate integration.

Connect the server, client, and account page#

Install the API key package at a version compatible with your Better Auth installation. Add this plugin to the existing server's plugins array, retaining its other plugins:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
import { apiKey } from "@better-auth/api-key";

export const personalApiKeys = apiKey({
  defaultPrefix: "app_",
  rateLimit: {
    enabled: true,
    timeWindow: 60_000,
    maxRequests: 60,
  },
});

This example permits 60 key verifications in a 60-second window. Choose limits for the endpoint's actual cost and expected workload. Keep the application's ordinary abuse controls as well; a per-key limit does not limit anonymous attempts or the number of keys an account can use.

Generate and review the auth schema for the installed release, then migrate it through your adapter's normal workflow. The API key table is part of Better Auth's storage. Enabling a UI flag does not create it.

Add apiKeyClient() from @better-auth/api-key/client to the existing createAuthClient plugins. Then merge this into the Stack provider's overrides.auth:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
import type { AuthPluginOverrides } from "@btst/better-auth-ui/client";

export const apiKeyUI = {
  apiKey: true,
} satisfies Partial<AuthPluginOverrides>;

Keep the auth and account client plugins mounted. With the normal /p mount, the account page is /p/account/api-keys. The BTST auth documentation covers that shared setup. The upstream API key documentation covers the server and client plugin contract.

Understand creation and one-time display#

The released create dialog submits a name and optional expiration, then displays the returned secret. It converts the selected number of days to seconds for expiresIn. Its initial “none” selection sends no custom expiration; without a server default, the key does not expire. A team's expiration policy therefore needs server configuration as well as a UI choice.

Copy the secret into the integration's secret store when it is created. The ordinary list/get responses do not recover the full secret. Keep hashing enabled on the server, and never send a key to analytics, put it in a URL, or commit it to a repository. If it is lost, create a replacement and revoke the old key.

The optional UI apiKey object supports a prefix and metadata. Those values arrive from the browser. A prefix is a label, and metadata is descriptive data; neither grants access to a project or organization. Enabling server metadata support must not turn client-supplied fields into trusted permissions.

Verify the key where the API is called#

Use the server API in the endpoint that consumes the key. This helper returns the authenticated owner's ID for this single user-owned configuration:

TS
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
import { auth } from "@/lib/auth";

export async function getApiKeyOwner(request: Request) {
  const suppliedKey = request.headers.get("x-api-key");
  if (!suppliedKey) return null;

  const result = await auth.api.verifyApiKey({
    body: { key: suppliedKey },
  });

  return result.valid && result.key ? result.key.referenceId : null;
}

The caller must reject a null result before reading or changing protected data. For a valid result, check that this owner may perform the operation on the requested resource. Do not accept a request's userId or metadata field as an ownership substitute. If you use key permissions, verify the required permissions too; they supplement resource authorization.

Do not enable session emulation merely to reuse a cookie-only route. This example leaves it off and verifies the key explicitly. Send the header over HTTPS from the integration's server or secret-aware runtime. A private API key embedded in a public browser bundle is exposed to its users.

Keep personal and organization keys separate#

In this companion release, the create dialog represents its organization selection in metadata. It does not send the dedicated organization ownership and configuration fields required by current organization-owned API key configurations. That distinction is visible in the pinned create-dialog source above.

Do not interpret a metadata organizationId as proof of membership or as an organization-owned key. Keep this guide's UI scoped to personal keys. For organization ownership, implement and test the upstream configuration, membership authorization, creation, listing, verification, and revocation together before exposing that workflow.

Test the full lifecycle#

Use two disposable accounts. Create a key as the first account, verify it through the consuming endpoint, and confirm that the second account cannot list or delete it. Check that missing and invalid keys cannot reach protected operations. Exercise the per-key rate limit without disabling it to make a test pass.

For rotation, create a replacement, move the integration to it, confirm a successful operation, then delete the old key and confirm rejection. Test expiration with your configured policy and actual storage. A creation toast proves neither that a downstream endpoint checks the key nor that resource authorization is correct.

The configuration fragments were type-checked, and focused in-memory Better Auth checks cover creation, secret handling, ownership, rate limiting, expiry, and deletion. They do not run a production migration or a complete account-page integration. See the session-management guide for the separate lifecycle of browser sessions.

In This Post

Connect the server, client, and account pageUnderstand creation and one-time displayVerify the key where the API is calledKeep personal and organization keys separateTest the full lifecycle