new·Earn with mozg — 20% of every monthSend somebody here and take a fifth of every plan payment they make, for as long as they keep paying — not a bounty on the first invoice. Your handle is the link, the window is thirty days, and the commission lands on your balance the second they pay. Free to join: if you have signed in, you already have the link. mozg.sh/earnall news →
mozg.beta
Sign in

Better Auth · Plugins · all subjects

api key plugin

60 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

API Key plugin for API key generation and management

Better Auth includes an API Key plugin that provides API key generation and management.

enableSessionForAPIKeys creates mock sessions from API keys

When enableSessionForAPIKeys is enabled, any endpoint in Better Auth called with a valid API key in headers will automatically create a mock session to represent the user. This allows the API key to be used for session-based operations. However, this is generally not recommended as a leaked API key can be used to impersonate a user.

enableSessionForAPIKeys only works with user-owned API keys

Session mocking via enableSessionForAPIKeys only works with user-owned API keys where references is set to 'user'. Organization-owned API keys cannot mock user sessions because there is no single user associated with the key.

Rate limiting applied once per request with enableSessionForAPIKeys

When enableSessionForAPIKeys is enabled, the API key is validated once per request and rate limiting is applied accordingly. If you manually verify an API key and then fetch a session separately, both operations will increment the rate limit counter. Using enableSessionForAPIKeys avoids this double increment.

Default API key header is x-api-key

The default header key for API keys is 'x-api-key'. This can be changed by setting the apiKeyHeaders option in the plugin options, which accepts an array of header names or a single string.

customAPIKeyGetter function to retrieve API keys from requests

You can pass a customAPIKeyGetter function to the plugin options that will be called with the HookEndpointContext. The function should return the API key string or null if the request is invalid. This allows custom logic for extracting API keys from requests beyond the standard header approach.

Multiple API key configurations with different prefixes and rate limits

You can define multiple API key configurations with different settings by passing an array of configuration objects to the apiKey plugin. Each configuration is identified by a unique configId and can have its own prefix, rate limits, permissions, and other options. This is useful for public vs private keys, read-only vs read-write keys, and different rate limits for different tiers.

Creating API keys with specific configId

When creating an API key, specify which configuration to use via the configId parameter in the body. Example: await auth.api.createApiKey({ body: { configId: 'public', userId: user.id } }). The configId determines which configuration's prefix and settings are applied to the generated key.

configId must match for get, update, delete operations

When performing get, update, or delete operations on API keys, you must pass the same configId that the key was created with. The verify operation resolves the key's own configuration, so it only needs configId when a configuration differs from the default in storage or hashing.

Filtering API keys by configId when listing

When listing API keys using authClient.apiKey.list(), you can filter by configId using the query parameter: await authClient.apiKey.list({ query: { configId: 'public' } }).

Global options as second argument to apiKey plugin

When passing multiple configurations to the apiKey plugin, you can provide global options like schema as a second argument: apiKey([{ configId: 'public' }, { configId: 'secret' }], { schema: { /* ... */ } }).

Organization-owned API keys can be configured with references setting

By default, API keys are owned by users. You can configure API keys to be owned by organizations by setting references: 'organization' in the configuration. This is useful for team-based applications where API keys should be shared across organization members.

Creating organization-owned API keys requires organizationId

When creating an organization-owned API key, pass the organizationId together with a user context. The user must be a member of the organization with permission to create API keys. Example: await auth.api.createApiKey({ body: { configId: 'org-keys', organizationId: 'org_123', userId: 'user_123' } }). The userId can be omitted when passing session headers.

Organization API key permissions structure

The API key plugin uses the following permissions: Create requires apiKey: ['create'], Read/List requires apiKey: ['read'], Update requires apiKey: ['update'], and Delete requires apiKey: ['delete']. These permissions control which actions organization members can perform on API keys based on their role.

Organization owners have full API key access by default

Organization owners (the creatorRole, default 'owner') automatically have full access to all API key operations (create, read, update, delete), regardless of explicit permission configuration.

Error codes for organization API key access denial

When access is denied for organization API keys, two error codes can be returned: USER_NOT_MEMBER_OF_ORGANIZATION when the user is not a member of the organization, and INSUFFICIENT_API_KEY_PERMISSIONS when the user doesn't have the required apiKey permission for the action.

API key object includes configId and referenceId fields

API key objects contain an id field, a configId field identifying which configuration the key belongs to, and a referenceId field for the owner (userId or organizationId based on the config's references setting). The owner type is determined by looking up the configuration's references setting.

Three storage modes for API keys: database, secondary-storage, secondary-storage with fallback

The API Key plugin supports three storage modes: 'database' (default) stores keys only in the database adapter; 'secondary-storage' stores keys only in secondary storage like Redis with no database fallback; and 'secondary-storage' with fallbackToDatabase: true checks secondary storage first, falls back to database if not found, and automatically populates secondary storage when falling back (cache warming). Write behavior with fallback writes to both database and secondary storage.

Custom storage methods for API keys override global secondaryStorage

You can provide custom storage methods specifically for API keys using the customStorage option, which overrides the global secondaryStorage configuration. The customStorage object contains get, set, and delete methods for implementing custom storage logic.

Rate limiting applies to API key validation operations

The built-in rate-limiting applies whenever an API key is validated, which includes when verifying an API key via the /api-key/verify endpoint and when using API keys for session creation if enableSessionForAPIKeys is enabled. Rate limiting applies to all endpoints that use the API key.

Sliding window rate limiting for API keys

The rate limiting system uses a sliding window approach. On first request with no previous lastRequest, the request is allowed and requestCount is set to 1. For subsequent requests within timeWindow, requestCount is incremented. If requestCount reaches rateLimitMax, the request is rejected with RATE_LIMITED error code. If time since last request exceeds timeWindow, the window resets with requestCount set to 1 and lastRequest updated. When rejected, the error response includes tryAgainIn value in milliseconds.

Customizing rate limits per API key on creation

When creating an API key, you can customize rate-limit options using rateLimitEnabled, rateLimitTimeWindow, and rateLimitMax fields in the body. Example: await auth.api.createApiKey({ body: { rateLimitEnabled: true, rateLimitTimeWindow: 1000 * 60 * 60 * 24, rateLimitMax: 10 } }). This allows per-key rate limit customization beyond the default plugin options.

Disabling rate limiting globally or per key

Rate limiting can be disabled globally by setting rateLimit.enabled: false in plugin options, or per key by setting rateLimitEnabled: false when creating or updating an API key. If rateLimitTimeWindow or rateLimitMax is null, rate limiting is effectively disabled for that key. When disabled, requests are still allowed but lastRequest is updated for tracking purposes.

Remaining count decremented on API key use

Whenever an API key is used, the remaining count is updated. If remaining is null, there is no cap to key usage. Otherwise, remaining is decremented by 1. If remaining reaches 0, the API key is disabled and removed.

refillInterval and refillAmount for automatic key refill

By default, refillInterval and refillAmount are set to null when an API key is created, meaning no automatic refill occurs. However, if both are set, whenever the API key is used, the system checks if time since last refill (or since creation if no refill occurred) exceeds refillInterval. If the interval has passed, remaining is reset to refillAmount (not incremented) and lastRefillAt is updated to current time.

API key expiration with expiresIn setting

By default, expiresAt is set to null when an API key is created, meaning it never expires. However, if expiresIn is set during creation, the API key will expire after the expiresIn time duration.

Custom key generation with customKeyGenerator

You can customize the key generation process using the customKeyGenerator option, which receives an object with length and prefix properties and must return the generated API key string. Example: customKeyGenerator: (options) => mySuperSecretApiKeyGenerator(options.length, options.prefix).

customKeyGenerator requires defaultKeyLength if not using length property

If you are not using the length property provided by customKeyGenerator, you must set the defaultKeyLength property to the length of generated keys. Example: when using crypto.randomUUID() which generates 36-character UUIDs, set defaultKeyLength: 36.

Custom API key validator with customAPIKeyValidator

You can provide a customAPIKeyValidator function that receives an object with ctx and key properties. The function should return a boolean indicating whether the key is valid. This allows improving performance by invalidating failed keys without querying the database, though the valid key must still be matched against the database.

Metadata storage for API keys requires enableMetadata configuration

To store metadata alongside API keys, ensure you haven't disabled the metadata feature in the plugin options by setting enableMetadata: true. Metadata can then be stored in the metadata field when creating an API key.

Creating API keys with metadata

When creating an API key, you can pass a metadata field in the body: await auth.api.createApiKey({ body: { metadata: { plan: 'premium' } } }). The metadata is then retrievable from the API key object.

Retrieving metadata from API key object

After creating an API key with metadata, you can retrieve it using getApiKey: const apiKey = await auth.api.getApiKey({ body: { keyId: 'your_api_key_id_here' } }); console.log(apiKey.metadata.plan);

API Key plugin features

The API Key plugin provides create, manage, and verify API keys; built-in rate limiting; custom expiration times, remaining count, and refill systems; metadata for API keys; custom prefix; sessions from API keys; secondary storage support for high-performance lookups; multiple configurations for different API key types; and organization-owned API keys in addition to user-owned keys.

API Key plugin installation steps

Install @better-auth/api-key package, add apiKey() to plugins in betterAuth config, run npx auth migrate or npx auth generate to add schema to database, and add apiKeyClient() to client plugins in createAuthClient.

Create API key endpoint and parameters

POST /api-key/create endpoint. Parameters: configId (string, optional, uses default if not provided), name (string, optional, default 'project-api-key'), expiresIn (number in seconds, optional, default 60 * 60 * 24 * 7), userId (string, optional, default 'user-id', required server-only for user-owned keys without session headers), organizationId (string, optional, default 'org-id', required for organization-owned keys), prefix (string, optional, default 'project-api-key'), remaining (number, optional, default 100, server-only), refillAmount (number, optional, default 100, server-only), refillInterval (number in milliseconds, optional, default 1000, server-only), rateLimitTimeWindow (number in milliseconds, optional, default 1000, server-only), rateLimitMax (number, optional, default 100, server-only), rateLimitEnabled (boolean, optional, default true, server-only), permissions (Record<string, string[]>, optional, server-only). Returns ApiKey object including the key value, or throws APIError.

API keys can be owned by organizations or users

API keys can be owned by either a user or an organization, depending on the configuration's references setting.

Verify API key endpoint and parameters

POST /api-key/verify endpoint, server-only. Parameters: configId (string, optional, defaults to validating against the key's own configuration when omitted), key (string, required, default 'your_api_key_here'), permissions (Record<string, string[]>, optional). Returns object with valid (boolean), error (object with message and code strings, or null), and key (Omit<ApiKey, 'key'> or null).

Get API key endpoint and parameters

GET /api-key/get endpoint, requires session. Parameters: configId (string, optional, uses default if not provided), id (string, required, default 'some-api-key-id'). Returns Omit<ApiKey, 'key'> or throws APIError.

Update API key endpoint and parameters

POST /api-key/update endpoint. Parameters: configId (string, optional, uses default if not provided), keyId (string, required, default 'some-api-key-id'), userId (string, optional, default 'some-user-id', server-only), name (string, optional, default 'some-api-key-name'), enabled (boolean, optional, default true, server-only), remaining (number, optional, default 100, server-only), refillAmount (number, optional, default 100, server-only), refillInterval (number in milliseconds, optional, default 1000, server-only), metadata (any or null, optional, default { 'key': 'value' }, server-only), expiresIn (number in seconds, optional, default 60 * 60 * 24 * 7, server-only), rateLimitEnabled (boolean, optional, default true, server-only), rateLimitTimeWindow (number in milliseconds, optional, default 1000, server-only), rateLimitMax (number, optional, default 100, server-only), permissions (Record<string, string[]>, optional, server-only). Returns API Key details except key value, or throws APIError.

Delete API key endpoint and parameters

POST /api-key/delete endpoint, requires session. Checks if user's ID matches the key owner before deletion. Parameters: configId (string, optional, uses default if not provided), keyId (string, required, default 'some-api-key-id'). Returns { success: boolean } or throws APIError.

List API keys endpoint and parameters

GET /api-key/list endpoint, requires session. Parameters: configId (string, optional, returns keys from all configurations if not provided), organizationId (string, optional, returns organization-owned keys if provided, returns user-owned keys for current session user if not provided), limit (number, optional), offset (number, optional), sortBy (string, optional, example values 'createdAt', 'name', 'expiresAt'), sortDirection (string, 'asc' or 'desc', optional). Returns { apiKeys: Omit<ApiKey, 'key'>[], total: number, limit?: number, offset?: number } or throws APIError.

List API keys pagination examples

Example 1: Get first 10 API keys for current user with authClient.apiKey.list({ query: { limit: 10 } }). Example 2: Get second page with limit: 10, offset: 10. Example 3: Sort by creation date newest first with sortBy: 'createdAt', sortDirection: 'desc'. Example 4: Combined pagination and sorting with limit: 20, offset: 0, sortBy: 'name', sortDirection: 'asc'. Example 5: List organization-owned keys with organizationId: 'org_123'. Example 6: List organization keys with specific config using organizationId: 'org_123', configId: 'public'.

Delete all expired API keys endpoint

POST /api-key/delete-all-expired-api-keys endpoint, server-only. Takes no parameters. Deletes all API keys with expired expiration dates. Expired keys are automatically deleted every time any apiKey plugin endpoints are called, rate-limited to 10 second cool down per call to prevent multiple database calls.

API key ownership: user vs organization

The `references` option determines whether API keys are owned by users or organizations. When `references` is set to "user", API keys are owned by users and require `userId` on creation. When set to "organization", API keys are owned by organizations and require `organizationId` on creation. This determines which entity controls the API key.

Three storage modes for API keys

API keys can be stored using three modes: "database" (store in the database adapter, default), "secondary-storage" (store in configured secondary storage like Redis), and custom storage via the `customStorage` option. When using "secondary-storage", keys are stored with patterns: `api-key:${hashedKey}` for primary lookup, `api-key:by-id:${id}` for ID lookup, and `api-key:by-ref:${referenceId}` for the reference's API key list.

enableSessionForAPIKeys configuration option

When `enableSessionForAPIKeys` is set to `true`, a valid API key can represent a session. The system will mock a session for the user if a valid API key is found in the request headers. Default is `false`.

Rate limiting API key validation

Rate limiting for API keys is configured via the `rateLimit` object with three options: `enabled` (boolean, default true) to turn rate limiting on/off, `timeWindow` (number in milliseconds) where each request is counted, and `maxRequests` (number) for the maximum allowed requests within the window. Once `maxRequests` is reached, requests are rejected until the `timeWindow` passes and resets.

API Key plugin configuration options table

API Key plugin configuration options: | Option | Type | Default | Description | |--------|------|---------|-------------| | configId | string | "default" | Unique identifier for configuration, required when using multiple configurations | | references | "user" \| "organization" | "user" | What the API key references; determines ownership | | apiKeyHeaders | string \| string[] | "x-api-key" | Header name(s) to check for API key | | customAPIKeyGetter | (ctx: GenericEndpointContext) => string \| null | N/A | Custom function to get API key from context | | customAPIKeyValidator | (options: { ctx: GenericEndpointContext; key: string; }) => boolean \| Promise<boolean> | N/A | Custom function to validate API key | | customKeyGenerator | (options: { length: number; prefix: string \| undefined; }) => string \| Promise<string> | N/A | Custom function to generate API key | | defaultKeyLength | number | 64 | Length of API key (doesn't include prefix) | | defaultPrefix | string | N/A | Prefix of API key | | maximumPrefixLength | number | N/A | Maximum length of prefix | | minimumPrefixLength | number | N/A | Minimum length of prefix | | requireName | boolean | false | Whether to require a name for API key | | maximumNameLength | number | N/A | Maximum length of API key name | | minimumNameLength | number | N/A | Minimum length of API key name | | enableMetadata | boolean | N/A | Whether to enable metadata for API key | | schema | InferOptionSchema<ReturnType<typeof apiKeySchema>> | N/A | Custom schema for API key plugin | | enableSessionForAPIKeys | boolean | false | API key can represent valid session and mock a session | | storage | "database" \| "secondary-storage" | "database" | Storage backend for API keys | | fallbackToDatabase | boolean | false | When storage is "secondary-storage", enable fallback to database | | customStorage | SecondaryStorage | N/A | Custom secondary storage for API keys | | deferUpdates | boolean | false | Defer non-critical updates to run after response sent | | disableKeyHashing | boolean | N/A | Disable hashing of API key (not recommended for security) | | permissions | { defaultPermissions?: Statements \| ((referenceId: string, ctx: GenericEndpointContext) => Statements \| Promise<Statements>) } | N/A | Permissions for the API key |

startingCharactersConfig options

The `startingCharactersConfig` object has two options: `shouldStore` (boolean, default true) determines whether to store the starting characters in the database (if false, `start` is set to null), and `charactersLength` (number, default 6) specifies the length of starting characters to store, including the prefix length.

keyExpiration configuration options

The `keyExpiration` object has four options: `defaultExpiresIn` (number | null, default null) sets default expiration time in milliseconds (null means no expiration), `disableCustomExpiresTime` (boolean, default false) disables custom expiration from client if true, `minExpiresIn` (number, default 1, in days) sets minimum allowed expiration, and `maxExpiresIn` (number, default 365, in days) sets maximum allowed expiration.

API key schema: apikey table fields

The `apikey` table contains the following fields: | Field | Type | Unique | Primary | Indexed | Optional | Description | |-------|------|--------|---------|---------|----------|-------------| | id | string | Yes | Yes | No | No | The ID of the API key | | configId | string | No | No | No | No | Configuration ID this key belongs to, default 'default' | | name | string | No | No | No | Yes | Name of the API key | | start | string | No | No | No | Yes | Starting characters of API key for UI identification | | prefix | string | No | No | No | Yes | API Key prefix, stored as plain text | | key | string | No | No | No | No | The hashed API key itself | | referenceId | string | No | No | Yes | No | ID of owner (user ID or organization ID based on config) | | refillInterval | number | No | No | No | Yes | Interval to refill key in milliseconds | | refillAmount | number | No | No | No | Yes | Amount to refill remaining count | | lastRefillAt | Date | No | No | No | Yes | Date and time key was last refilled | | enabled | boolean | No | No | No | Yes | Whether API key is enabled | | rateLimitEnabled | boolean | No | No | No | Yes | Whether API key has rate limiting enabled | | rateLimitTimeWindow | number | No | No | No | Yes | Time window in milliseconds for rate limit | | rateLimitMax | number | No | No | No | Yes | Maximum requests allowed within rateLimitTimeWindow | | requestCount | number | No | No | No | Yes | Number of requests made within rate limit window | | remaining | number | No | No | No | Yes | Number of requests remaining | | lastRequest | Date | No | No | No | Yes | Date and time of last request made to key | | expiresAt | Date | No | No | No | Yes | Date and time when key will expire | | createdAt | Date | No | No | No | No | Date and time API key was created | | updatedAt | Date | No | No | No | No | Date and time API key was updated | | permissions | string | No | No | No | Yes | Permissions of the key | | metadata | string | No | No | No | Yes | Additional metadata stored with key |

API key permissions structure

Permissions for API keys follow a resource-based structure as a record of resource types to arrays of allowed actions. Example: `{ files: ["read", "write", "delete"], users: ["read"], projects: ["read", "write"] }`. When verifying an API key, all required permissions must be present in the API key's permissions for validation to succeed.

Setting default permissions for API keys

Default permissions can be configured as a static object or as a function that returns permissions dynamically. When using a function, it receives `referenceId` (either userId or orgId depending on config) and `ctx` parameters, allowing you to fetch user/org role or other data to determine permissions.

Creating API key with permissions

When creating an API key using `auth.api.createApiKey()`, you can specify custom permissions in the body. Example: `await auth.api.createApiKey({ body: { name: "My API Key", permissions: { files: ["read", "write"], users: ["read"] }, userId: "userId" } })`.

Verifying API key with required permissions

To verify an API key and check if it has required permissions, use `auth.api.verifyApiKey()` with the key and required permissions. If `result.valid` is true, the API key is valid and has the required permissions. Example: `await auth.api.verifyApiKey({ body: { key: "your_api_key_here", permissions: { files: ["read"] } } })`.

Updating API key permissions

To update permissions of an existing API key, use `auth.api.updateApiKey()` with the keyId and new permissions. Example: `await auth.api.updateApiKey({ body: { keyId: existingApiKeyId, permissions: { files: ["read", "write", "delete"], users: ["read", "write"] } }, headers: user_headers })`.

Secondary storage configuration for API keys

When using secondary storage for API keys, configure the `secondaryStorage` object in Better Auth options with three methods: `get(key)`, `set(key, value, ttl)`, and `delete(key)`. API keys with expiration dates automatically set a TTL in secondary storage for cleanup. Example using Redis is provided showing the `get`, `set`, and `delete` implementations.

Custom secondary storage for API keys

The `customStorage` option allows using a different storage backend specifically for API keys instead of global secondary storage. Custom storage takes precedence over global secondary storage and must implement the full secondary-storage contract including `get`, `getAndDelete`, `increment`, `set`, and `delete` methods.

deferUpdates configuration for serverless platforms

When `deferUpdates` is set to `true`, non-critical updates (rate limiting counters, timestamps, remaining count) are deferred to run after the response is sent using the global `backgroundTasks` handler. This improves response times on serverless platforms. Requires `backgroundTasks.handler` to be configured. Enabling this introduces eventual consistency where optimistic data is returned before database updates.

Breaking change: userId replaced with referenceId

The `userId` field has been replaced with `referenceId` in API responses. API responses now return `referenceId` instead of `userId`. The owner type (user vs organization) is determined by the configuration's `references` setting, not stored on each key.

Give your agent this brain