docs/content/docs/plugins/api-key/advanced.mdx
Any time an endpoint in Better Auth is called that has a valid API key in the headers, you can automatically create a mock session to represent the user by enabling enableSessionForAPIKeys option.
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey({
enableSessionForAPIKeys: true,
}),
],
});
<Tabs items={['Server']}> <Tab value="Server"> ```ts import { auth } from "@/lib/auth"
const session = await auth.api.getSession({
headers: new Headers({
'x-api-key': apiKey,
}),
});
```
The default header key is x-api-key, but this can be changed by setting the apiKeyHeaders option in the plugin options.
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey({
apiKeyHeaders: ["x-api-key", "xyz-api-key"], // or you can pass just a string, eg: "x-api-key"
}),
],
});
Or optionally, you can pass a customAPIKeyGetter function to the plugin options, which will be called with the HookEndpointContext, and from there, you should return the API key, or null if the request is invalid.
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey({
customAPIKeyGetter: (ctx) => {
const has = ctx.request.headers.has("x-api-key");
if (!has) return null;
return ctx.request.headers.get("x-api-key");
},
}),
],
});
You can define multiple API key configurations with different settings. Each configuration is identified by a unique configId and can have its own prefix, rate limits, permissions, and other options.
This is useful when you need different types of API keys for different purposes, such as:
Pass an array of configuration objects to the apiKey plugin:
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey([
{
configId: "public",
defaultPrefix: "pk_",
rateLimit: {
enabled: true,
maxRequests: 100,
timeWindow: 1000 * 60 * 60, // 1 hour
},
},
{
configId: "secret",
defaultPrefix: "sk_",
enableMetadata: true,
rateLimit: {
enabled: true,
maxRequests: 1000,
timeWindow: 1000 * 60 * 60, // 1 hour
},
},
]),
],
});
When creating an API key, specify which configuration to use via the configId parameter:
import { auth } from "@/lib/auth"
// Create a public key
const publicKey = await auth.api.createApiKey({
body: {
configId: "public",
userId: user.id,
},
});
// Result: pk_...
// Create a secret key
const secretKey = await auth.api.createApiKey({
body: {
configId: "secret",
userId: user.id,
metadata: { plan: "premium" },
},
});
// Result: sk_...
All API key operations support the configId parameter to specify which configuration to use for the lookup. This is important when different configurations have different storage backends (e.g., database vs Redis):
// Get an API key using a specific config
const key = await auth.api.getApiKey({
query: {
id: keyId,
configId: "secret"
},
headers,
});
// Update an API key using a specific config
await auth.api.updateApiKey({
body: {
keyId: keyId,
configId: "secret",
name: "Updated Name",
},
});
// Delete an API key using a specific config
await auth.api.deleteApiKey({
body: {
keyId: keyId,
configId: "secret",
},
headers,
});
// Verify an API key using a specific config
const result = await auth.api.verifyApiKey({
body: {
key: apiKeyValue,
configId: "secret",
},
});
When listing API keys, you can filter by configId:
// List only public keys
const publicKeys = await authClient.apiKey.list({
query: { configId: "public" }
});
// List only secret keys
const secretKeys = await authClient.apiKey.list({
query: { configId: "secret" }
});
You can also pass global options (like schema) as a second argument:
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey(
[
{ configId: "public", defaultPrefix: "pk_" },
{ configId: "secret", defaultPrefix: "sk_" },
],
{
schema: {
// Custom schema options
},
}
),
],
});
By default, API keys are owned by users. However, you can configure API keys to be owned by organizations instead. This is useful for team-based applications where API keys should be shared across organization members.
Set references: "organization" in your configuration:
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey([
{
configId: "user-keys",
defaultPrefix: "user_",
references: "user", // Default - owned by users
},
{
configId: "org-keys",
defaultPrefix: "org_",
references: "organization", // Owned by organizations
},
]),
],
});
When creating an organization-owned API key, pass the organizationId instead of userId:
import { auth } from "@/lib/auth"
const orgKey = await auth.api.createApiKey({
body: {
configId: "org-keys",
organizationId: "org_123", // Required for org-owned keys
},
});
Organization-owned API keys use the organization plugin's role-based access control system. To manage organization API keys, users must:
apiKey permission for the action they're performingThe API key plugin uses the following permissions:
| Action | Permission | Description |
|---|---|---|
| Create | apiKey: ["create"] | Create new organization API keys |
| Read/List | apiKey: ["read"] | View and list organization API keys |
| Update | apiKey: ["update"] | Modify organization API keys |
| Delete | apiKey: ["delete"] | Delete organization API keys |
By default, organization owners have full access to all API key operations. For other roles (like admin or member), you need to explicitly configure apiKey permissions in your organization plugin setup.
Here's how to configure roles with API key permissions:
import { betterAuth } from "better-auth"
import { organization } from "better-auth/plugins"
import { apiKey } from "@better-auth/api-key"
import { createAccessControl } from "better-auth/plugins/access"
// Define your access control statements including apiKey
const statements = {
// ... other statements
// Add apiKey permissions
apiKey: ["create", "read", "update", "delete"], // [!code highlight]
} as const;
const ac = createAccessControl(statements);
// Define roles with specific apiKey permissions
const adminRole = ac.newRole({
// ... other statements
// Admins can manage API keys
apiKey: ["create", "read", "update", "delete"], // [!code highlight]
});
const memberRole = ac.newRole({
// ... other statements
// Members can only view API keys
apiKey: ["read"], // [!code highlight]
});
export const auth = betterAuth({
plugins: [
organization({
ac,
roles: {
admin: adminRole,
member: memberRole,
},
async sendInvitationEmail() {},
}),
apiKey([
{
configId: "org-keys",
defaultPrefix: "org_",
references: "organization",
},
]),
],
});
// Admin can create, read, update, delete org API keys
const key = await auth.api.createApiKey({
body: { configId: "org-keys", organizationId: "org_123" },
headers: adminHeaders,
});
// Member with only "read" permission can list keys
const keys = await client.apiKey.list(
{ query: { organizationId: "org_123" } },
{ headers: memberHeaders },
);
// Member trying to create a key will get FORBIDDEN error
const result = await client.apiKey.create(
{ configId: "org-keys", organizationId: "org_123" },
{ headers: memberHeaders },
);
// Error: INSUFFICIENT_API_KEY_PERMISSIONS
When access is denied, the following error codes are returned:
USER_NOT_MEMBER_OF_ORGANIZATION: The user is not a member of the organizationINSUFFICIENT_API_KEY_PERMISSIONS: The user doesn't have the required apiKey permission for the actionAPI keys include a configId to identify which configuration they belong to, and a referenceId for the owner:
type ApiKey = {
id: string;
configId: string; // The configuration this key belongs to
referenceId: string; // The owner ID (userId or organizationId based on config)
// ... other fields
};
The owner type (user vs organization) is determined by looking up the configuration:
const apiKey = await auth.api.getApiKey({
query: { id: keyId },
headers,
});
// The owner type is determined by the config's `references` setting
// For org-keys config (references: "organization"):
console.log(`Key owned by: ${apiKey.referenceId}`);
The API Key plugin supports multiple storage modes for flexible API key management, allowing you to choose the best strategy for your use case.
"database" (Default)Store API keys only in the database adapter. This is the default mode and requires no additional configuration.
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey({
storage: "database", // Default, can be omitted
}),
],
});
"secondary-storage"Store API keys only in secondary storage (e.g., Redis). No fallback to database. Best for high-performance scenarios where all keys are migrated to secondary storage.
import { createClient } from "redis";
import { betterAuth } from "better-auth";
import { apiKey } from "@better-auth/api-key";
const redis = createClient();
await redis.connect();
export const auth = betterAuth({
secondaryStorage: {
get: async (key) => await redis.get(key),
set: async (key, value, ttl) => {
if (ttl) await redis.set(key, value, { EX: ttl });
else await redis.set(key, value);
},
delete: async (key) => await redis.del(key),
},
plugins: [
apiKey({
storage: "secondary-storage",
}),
],
});
Check secondary storage first, then fallback to database if not found.
Read behavior:
Write behavior:
import { betterAuth } from "better-auth"
import { createClient } from "redis";
const redis = createClient();
await redis.connect();
export const auth = betterAuth({
secondaryStorage: {
get: async (key) => await redis.get(key),
set: async (key, value, ttl) => {
if (ttl) await redis.set(key, value, { EX: ttl });
else await redis.set(key, value);
},
delete: async (key) => await redis.del(key),
},
plugins: [
apiKey({
storage: "secondary-storage",
fallbackToDatabase: true,
}),
],
});
You can provide custom storage methods specifically for API keys, overriding the global secondaryStorage configuration:
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey({
storage: "secondary-storage",
customStorage: {
get: async (key) => {
// Custom get logic for API keys
return await customStorage.get(key);
},
set: async (key, value, ttl) => {
// Custom set logic for API keys
await customStorage.set(key, value, ttl);
},
delete: async (key) => {
// Custom delete logic for API keys
await customStorage.delete(key);
},
},
}),
],
});
Every API key can have its own rate limit settings. The built-in rate-limiting applies whenever an API key is validated, which includes:
/api-key/verify endpointenableSessionForAPIKeys is enabled), rate limiting applies to all endpoints that use the API keyFor other endpoints/methods that don't use API keys, you should utilize Better Auth's built-in rate-limiting.
<Callout type="warn"> **Double Rate-Limit Increment**: If you manually verify an API key using `verifyApiKey()` and then fetch a session using `getSession()` with the same API key header, both operations will increment the rate limit counter, resulting in two increments for a single request. To avoid this, either:enableSessionForAPIKeys: true and let Better Auth handle session creation automatically (recommended)You can refer to the rate-limit default configurations below in the API Key plugin options.
An example default value:
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey({
rateLimit: {
enabled: true,
timeWindow: 1000 * 60 * 60 * 24, // 1 day
maxRequests: 10, // 10 requests per day
},
}),
],
});
For each API key, you can customize the rate-limit options on create.
<Callout> You can only customize the rate-limit options on the server auth instance. </Callout>import { auth } from "@/lib/auth"
const apiKey = await auth.api.createApiKey({
body: {
rateLimitEnabled: true,
rateLimitTimeWindow: 1000 * 60 * 60 * 24, // 1 day
rateLimitMax: 10, // 10 requests per day
},
headers: await headers() // headers containing the user's session token
});
The rate limiting system uses a sliding window approach:
First Request: When an API key is used for the first time (no previous lastRequest), the request is allowed and requestCount is set to 1.
Within Window: For subsequent requests within the timeWindow, the requestCount is incremented. If requestCount reaches rateLimitMax, the request is rejected with a RATE_LIMITED error code.
Window Reset: If the time since the last request exceeds the timeWindow, the window resets: requestCount is reset to 1 and lastRequest is updated to the current time.
Rate Limit Exceeded: When a request is rejected due to rate limiting, the error response includes a tryAgainIn value (in milliseconds) indicating how long to wait before the window resets.
Disabling Rate Limiting:
rateLimit.enabled: false in plugin optionsrateLimitEnabled: false when creating or updating an API keyrateLimitTimeWindow or rateLimitMax is null, rate limiting is effectively disabled for that keyWhen rate limiting is disabled (globally or per-key), requests are still allowed but lastRequest is updated for tracking purposes.
The remaining count is the number of requests left before the API key is disabled.
The refill interval is the interval in milliseconds where the remaining count is refilled when the interval has passed since the last refill (or since creation if no refill has occurred yet).
The expiration time is the expiration date of the API key.
Whenever an API key is used, the remaining count is updated.
If the remaining count is null, then there is no cap to key usage.
Otherwise, the remaining count is decremented by 1.
If the remaining count is 0, then the API key is disabled & removed.
Whenever an API key is created, the refillInterval and refillAmount are set to null by default.
This means that the API key will not be refilled automatically.
However, if both refillInterval & refillAmount are set, then whenever the API key is used:
refillIntervalremaining count is reset to refillAmount (not incremented)lastRefillAt timestamp is updated to the current timeWhenever an API key is created, the expiresAt is set to null.
This means that the API key will never expire.
However, if the expiresIn is set, then the API key will expire after the expiresIn time.
You can customize the key generation and verification process straight from the plugin options.
Here's an example:
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey({
customKeyGenerator: (options: {
length: number;
prefix: string | undefined;
}) => {
const apiKey = mySuperSecretApiKeyGenerator(
options.length,
options.prefix
);
return apiKey;
},
customAPIKeyValidator: async ({ ctx, key }) => {
const res = await keyService.verify(key)
return res.valid
},
}),
],
});
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey({
customKeyGenerator: () => {
return crypto.randomUUID();
},
defaultKeyLength: 36, // Or whatever the length is
}),
],
});
If an API key is validated from your customAPIKeyValidator, we still must match that against the database's key.
However, by providing this custom function, you can improve the performance of the API key verification process,
as all failed keys can be invalidated without having to query your database.
We allow you to store metadata alongside your API keys. This is useful for storing information about the key, such as a subscription plan for example.
To store metadata, make sure you haven't disabled the metadata feature in the plugin options.
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"
export const auth = betterAuth({
plugins: [
apiKey({
enableMetadata: true,
}),
],
});
Then, you can store metadata in the metadata field of the API key object.
import { auth } from "@/lib/auth"
const apiKey = await auth.api.createApiKey({
body: {
metadata: { // [!code highlight]
plan: "premium", // [!code highlight]
}, // [!code highlight]
},
});
You can then retrieve the metadata from the API key object.
import { auth } from "@/lib/auth"
const apiKey = await auth.api.getApiKey({
body: {
keyId: "your_api_key_id_here",
},
});
console.log(apiKey.metadata.plan); // "premium"