Back to Better Auth

Reference

docs/content/docs/plugins/api-key/reference.mdx

1.6.1416.9 KB
Original Source

API Key plugin options

configId <span className="opacity-70">string</span>

A unique identifier for this configuration. Required when using multiple configurations. Default is "default".

references <span className="opacity-70">"user" | "organization"</span>

What the API key references. This determines ownership over the API key. Default is "user".

  • "user": API keys are owned by users (requires userId on creation)
  • "organization": API keys are owned by organizations (requires organizationId on creation)

apiKeyHeaders <span className="opacity-70">string | string[];</span>

The header name to check for API key. Default is x-api-key.

customAPIKeyGetter <span className="opacity-70">(ctx: GenericEndpointContext) => string | null</span>

A custom function to get the API key from the context.

customAPIKeyValidator <span className="opacity-70">(options: { ctx: GenericEndpointContext; key: string; }) => boolean | Promise<boolean></span>

A custom function to validate the API key.

customKeyGenerator <span className="opacity-70">(options: { length: number; prefix: string | undefined; }) => string | Promise<string></span>

A custom function to generate the API key.

startingCharactersConfig <span className="opacity-70">{ shouldStore?: boolean; charactersLength?: number; }</span>

Customize the starting characters configuration.

<Accordions> <Accordion title="startingCharactersConfig Options"> `shouldStore` <span className="opacity-70">`boolean`</span>
Whether to store the starting characters in the database.
If false, we will set `start` to `null`.
Default is `true`.

`charactersLength` <span className="opacity-70">`number`</span>

The length of the starting characters to store in the database.
This includes the prefix length.
Default is `6`.
</Accordion> </Accordions>

defaultKeyLength <span className="opacity-70">number</span>

The length of the API key. Longer is better. Default is 64. (Doesn't include the prefix length)

defaultPrefix <span className="opacity-70">string</span>

The prefix of the API key.

Note: We recommend you append an underscore to the prefix to make the prefix more identifiable. (eg hello_)

maximumPrefixLength <span className="opacity-70">number</span>

The maximum length of the prefix.

minimumPrefixLength <span className="opacity-70">number</span>

The minimum length of the prefix.

requireName <span className="opacity-70">boolean</span>

Whether to require a name for the API key. Default is false.

maximumNameLength <span className="opacity-70">number</span>

The maximum length of the name.

minimumNameLength <span className="opacity-70">number</span>

The minimum length of the name.

enableMetadata <span className="opacity-70">boolean</span>

Whether to enable metadata for an API key.

keyExpiration <span className="opacity-70">{ defaultExpiresIn?: number | null; disableCustomExpiresTime?: boolean; minExpiresIn?: number; maxExpiresIn?: number; }</span>

Customize the key expiration.

<Accordions> <Accordion title="keyExpiration options"> `defaultExpiresIn` <span className="opacity-70">`number | null`</span>
The default expires time in milliseconds.
If `null`, then there will be no expiration time.
Default is `null`.

`disableCustomExpiresTime` <span className="opacity-70">`boolean`</span>

Whether to disable the expires time passed from the client.
If `true`, the expires time will be based on the default values.
Default is `false`.

`minExpiresIn` <span className="opacity-70">`number`</span>

The minimum expiresIn value allowed to be set from the client. in days.
Default is `1`.

`maxExpiresIn` <span className="opacity-70">`number`</span>

The maximum expiresIn value allowed to be set from the client. in days.
Default is `365`.
</Accordion> </Accordions>

rateLimit <span className="opacity-70">{ enabled?: boolean; timeWindow?: number; maxRequests?: number; }</span>

Customize the rate-limiting.

<Accordions> <Accordion title="rateLimit options"> `enabled` <span className="opacity-70">`boolean`</span>
Whether to enable rate limiting. (Default true)

`timeWindow` <span className="opacity-70">`number`</span>

The duration in milliseconds where each request is counted.
Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.

`maxRequests` <span className="opacity-70">`number`</span>

Maximum amount of requests allowed within a window.
Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset.
</Accordion> </Accordions>

schema <span className="opacity-70">InferOptionSchema<ReturnType<typeof apiKeySchema>></span>

Custom schema for the API key plugin.

enableSessionForAPIKeys <span className="opacity-70">boolean</span>

An API Key can represent a valid session, so we can mock a session for the user if we find a valid API key in the request headers. Default is false.

storage <span className="opacity-70">"database" | "secondary-storage"</span>

Storage backend for API keys. Default is "database".

  • "database": Store API keys in the database adapter (default)
  • "secondary-storage": Store API keys in the configured secondary storage (e.g., Redis)

fallbackToDatabase <span className="opacity-70">boolean</span>

When storage is "secondary-storage", enable fallback to database if key is not found in secondary storage. Default is false.

<Callout> When `storage` is set to `"secondary-storage"`, you must configure `secondaryStorage` in your Better Auth options. API keys will be stored using key-value patterns:
  • api-key:${hashedKey} - Primary lookup by hashed key
  • api-key:by-id:${id} - Lookup by ID
  • api-key:by-ref:${referenceId} - Reference's API key list (user or organization)

If an API key has an expiration date (expiresAt), a TTL will be automatically set in secondary storage to ensure automatic cleanup. </Callout>

ts
import { betterAuth } from "better-auth";
import { apiKey } from "@better-auth/api-key"

export const auth = betterAuth({
  secondaryStorage: {
    get: async (key) => {
      return 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",
    }),
  ],
});

customStorage <span className="opacity-70">{ get: (key: string) => Promise<unknown> | unknown; set: (key: string, value: string, ttl?: number) => Promise<void | null | unknown> | void; delete: (key: string) => Promise<void | null | string> | void; }</span>

Custom storage methods for API keys. If provided, these methods will be used instead of ctx.context.secondaryStorage. Custom methods take precedence over global secondary storage.

Useful when you want to use a different storage backend specifically for API keys, or when you need custom logic for storage operations.

ts
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) => await customStorage.get(key),
        set: async (key, value, ttl) => await customStorage.set(key, value, ttl),
        delete: async (key) => await customStorage.delete(key), 
      },
    }),
  ],
});

deferUpdates <span className="opacity-70">boolean</span>

Defer non-critical updates (rate limiting counters, timestamps, remaining count) to run after the response is sent using the global backgroundTasks handler. This can significantly improve response times on serverless platforms. Default is false.

Requires backgroundTasks.handler to be configured in the main auth options.

<Callout type="warn"> Enabling this introduces eventual consistency where the response returns optimistic data before the database is updated. Only enable if your application can tolerate this trade-off for improved latency. </Callout>

<Tabs items={["Vercel", "Cloudflare Workers"]}> <Tab value="Vercel"> ```ts import { waitUntil } from "@vercel/functions";

export const auth = betterAuth({
  advanced: { 
      backgroundTasks: {
         handler: waitUntil,
      },
  }
  plugins: [
    apiKey({
      deferUpdates: true,
    }),
  ],
});
```
</Tab> <Tab value="Cloudflare Workers"> ```ts import { AsyncLocalStorage } from "node:async_hooks";
const execCtxStorage = new AsyncLocalStorage<ExecutionContext>();

export const auth = betterAuth({
  advanced: { 
      backgroundTasks: {
         handler: waitUntil,
      },
  }
  plugins: [
    apiKey({
      deferUpdates: true,
    }),
  ],
});

// In your request handler, wrap with execCtxStorage.run(ctx, ...)
```
</Tab> </Tabs>

permissions <span className="opacity-70">{ defaultPermissions?: Statements | ((referenceId: string, ctx: GenericEndpointContext) => Statements | Promise<Statements>) }</span>

Permissions for the API key.

Read more about permissions below.

<Accordions> <Accordion title="permissions Options"> `defaultPermissions` <span className="opacity-70">`Statements | ((referenceId: string, ctx: GenericEndpointContext) => Statements | Promise<Statements>)`</span>
The default permissions for the API key. The `referenceId` is either the user ID or organization ID depending on the `references` setting.
</Accordion> </Accordions>

disableKeyHashing <span className="opacity-70">boolean</span>

Disable hashing of the API key.

<Callout type="warn"> Security Warning: It's strongly recommended to not disable hashing. Storing API keys in plaintext makes them vulnerable to database breaches, potentially exposing all your users' API keys. </Callout>

Permissions

API keys can have permissions associated with them, allowing you to control access at a granular level. Permissions are structured as a record of resource types to arrays of allowed actions.

Setting Default Permissions

You can configure default permissions that will be applied to all newly created API keys:

ts
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"

export const auth = betterAuth({
  plugins: [
    apiKey({
      permissions: {
        defaultPermissions: {
          files: ["read"],
          users: ["read"],
        },
      },
    }),
  ],
});

You can also provide a function that returns permissions dynamically:

ts
import { betterAuth } from "better-auth"
import { apiKey } from "@better-auth/api-key"

export const auth = betterAuth({
  plugins: [
    apiKey({
      permissions: {
        defaultPermissions: async (referenceId, ctx) => {
          // referenceId is either userId or orgId depending on config
          // Fetch user/org role or other data to determine permissions
          return {
            files: ["read"],
            users: ["read"],
          };
        },
      },
    }),
  ],
});

Creating API Keys with Permissions

When creating an API key, you can specify custom permissions:

ts
import { auth } from "@/lib/auth"

const apiKey = await auth.api.createApiKey({
  body: {
    name: "My API Key",
    permissions: {
      files: ["read", "write"],
      users: ["read"],
    },
    userId: "userId",
  },
});

Verifying API Keys with Required Permissions

When verifying an API key, you can check if it has the required permissions:

ts
import { auth } from "@/lib/auth"

const result = await auth.api.verifyApiKey({
  body: {
    key: "your_api_key_here",
    permissions: {
      files: ["read"],
    },
  },
});

if (result.valid) {
  // API key is valid and has the required permissions
} else {
  // API key is invalid or doesn't have the required permissions
}

Updating API Key Permissions

You can update the permissions of an existing API key:

ts
import { auth } from "@/lib/auth"

const apiKey = await auth.api.updateApiKey({
  body: {
    keyId: existingApiKeyId,
    permissions: {
      files: ["read", "write", "delete"],
      users: ["read", "write"],
    },
  },
  headers: user_headers,
});

Permissions Structure

Permissions follow a resource-based structure:

ts
type Permissions = {
  [resourceType: string]: string[];
};

// Example:
const permissions = {
  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.

Schema

Table: apikey

export const apiKeyTableFields = [ { name: "id", type: "string", description: "The ID of the API key.", isUnique: true, isPrimaryKey: true, }, { name: "configId", type: "string", description: "The configuration ID this key belongs to. Default is 'default'.", }, { name: "name", type: "string", description: "The name of the API key.", isOptional: true, }, { name: "start", type: "string", description: "The starting characters of the API key. Useful for showing the first few characters of the API key in the UI for the users to easily identify.", isOptional: true, }, { name: "prefix", type: "string", description: "The API Key prefix. Stored as plain text.", isOptional: true, }, { name: "key", type: "string", description: "The hashed API key itself.", }, { name: "referenceId", type: "string", description: "The ID of the owner (user ID or organization ID based on the config's references setting).", isIndexed: true, }, { name: "refillInterval", type: "number", description: "The interval to refill the key in milliseconds.", isOptional: true, }, { name: "refillAmount", type: "number", description: "The amount to refill the remaining count of the key.", isOptional: true, }, { name: "lastRefillAt", type: "Date", description: "The date and time when the key was last refilled.", isOptional: true, }, { name: "enabled", type: "boolean", description: "Whether the API key is enabled.", isOptional: true, }, { name: "rateLimitEnabled", type: "boolean", description: "Whether the API key has rate limiting enabled.", isOptional: true, }, { name: "rateLimitTimeWindow", type: "number", description: "The time window in milliseconds for the rate limit.", isOptional: true, }, { name: "rateLimitMax", type: "number", description: "The maximum number of requests allowed within the rateLimitTimeWindow.", isOptional: true, }, { name: "requestCount", type: "number", description: "The number of requests made within the rate limit time window.", isOptional: true, }, { name: "remaining", type: "number", description: "The number of requests remaining.", isOptional: true, }, { name: "lastRequest", type: "Date", description: "The date and time of the last request made to the key.", isOptional: true, }, { name: "expiresAt", type: "Date", description: "The date and time when the key will expire.", isOptional: true, }, { name: "createdAt", type: "Date", description: "The date and time the API key was created.", }, { name: "updatedAt", type: "Date", description: "The date and time the API key was updated.", }, { name: "permissions", type: "string", description: "The permissions of the key.", isOptional: true, }, { name: "metadata", type: "string", isOptional: true, description: "Any additional metadata you want to store with the key.", }, ];

<DatabaseTable name="apikey" fields={apiKeyTableFields} />

Migration from Previous Versions

If you're upgrading from a previous version, you'll need to migrate the userId field to the new reference system:

sql
-- Add new columns
ALTER TABLE apikey ADD COLUMN config_id VARCHAR(255) NOT NULL DEFAULT 'default';
ALTER TABLE apikey ADD COLUMN reference_id VARCHAR(255);

-- Migrate existing data (copy userId to referenceId)
UPDATE apikey SET reference_id = user_id WHERE reference_id IS NULL;

-- Make reference_id required and add indexes
ALTER TABLE apikey ALTER COLUMN reference_id SET NOT NULL;
CREATE INDEX idx_apikey_reference_id ON apikey(reference_id);
CREATE INDEX idx_apikey_config_id ON apikey(config_id);

-- Optionally drop the old column after verifying migration
-- ALTER TABLE apikey DROP COLUMN user_id;
<Callout type="warn"> **Breaking Change**: The `userId` field has been replaced with `referenceId`. 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. </Callout>