Skip to Content
Delmare Digital

API keys

Enable Better Auth API keys, manage them from the Payload admin, and enforce per-collection read and write scopes in access control.

API keys come from Better Auth's @better-auth/api-key plugin. Once it is configured, the plugin adds an API key management view to the admin and gives you access functions that check a key's scopes.

Setup

npm i @better-auth/api-key
src/lib/auth/config.ts
import { apiKey } from '@better-auth/api-key'

export const betterAuthOptions = {
  plugins: [
    apiKey(),
  ],
}

The management UI and the permission helpers work once the apiKey plugin is configured. The admin view appears when the plugin can see your Better Auth options; see Security management.

API-key security parameters (expiry, rate limit, key length) are yours to set on apiKey(), as in any Better Auth setup. The plugin does not impose defaults.

Sending a key

Send keys in the x-api-key header. That is what Better Auth's api-key plugin reads by default. Authorization: Bearer <key> is only treated as an API key if you configure the plugin's apiKeyHeaders or customAPIKeyGetter; otherwise such a request authenticates as nobody.

Permissions model

Each collection has two permission levels: read (view only) and write (create, update and delete). write implies read. Permissions use Better Auth's native format, Record<string, string[]> keyed by collection slug, and the admin UI generates the choices from your Payload collections.

Configure the management UI under admin.apiKey on createBetterAuthPlugin():

src/payload.config.ts
createBetterAuthPlugin({
  createAuth,
  admin: {
    apiKey: {
      excludeCollections: ['internal-logs'],
      requiredRole: 'admin',
    },
  },
})

Prop

Type

The default exclusions do not match the generated API-key collection slug (apikeys, or apikey with usePlural: false). To hide it from the permission picker, pass an explicit excludeCollections list containing the actual slug and every other collection you want excluded. An explicit list replaces the defaults.

Enforcing permissions

Use these in Payload access control. They prefer the key's scopes that betterAuthStrategy already resolved onto req.user (req.user.apiKeyScopes), which does not spend the key's usage quota or a rate-limit slot. When those are not available, they verify the key with Better Auth at most once per request, however many permissions a check asks for.

src/collections/Posts.ts
import {
  requirePermission,
  allowSessionOrPermission,
} from '@delmaredigital/payload-better-auth'

access: {
  read: allowSessionOrPermission('posts', 'read'), // session users or keys with posts:read
  create: requirePermission('posts', 'write'),
  update: requirePermission('posts', 'write'),
  delete: requirePermission('posts', 'write'),
}
HelperGrants access when
requirePermission(resource, action, config?)The request carries a key with that permission.
requireAnyPermission(checks, config?)The key has any of the { resource, action } checks.
requireAllPermissions(checks, config?)The key has all of them.
allowSessionOrPermission(resource, action, config?)A signed-in (non-key) session, or a key with that permission.
allowSessionOrAnyPermission(checks, config?)A signed-in session, or a key with any of the checks.
requireApiKey(config?)Any valid key, with no specific permission.
import { requireAnyPermission, requireAllPermissions, requireApiKey } from '@delmaredigital/payload-better-auth'

// Require ANY of these permissions
read: requireAnyPermission([
  { resource: 'posts', action: 'read' },
  { resource: 'pages', action: 'read' },
])

// Require ALL permissions
delete: requireAllPermissions([
  { resource: 'posts', action: 'write' },
  { resource: 'admin', action: 'write' },
])

// Just verify the key is valid (no specific permissions)
read: requireApiKey()

The optional config takes allowAuthenticatedUsers (default false; the allowSession… helpers set it to true) and extractApiKey, a function that returns the key from the request. The default, extractApiKeyFromRequest, reads x-api-key and Authorization.

A user resolved from an API key never counts as a session user, so a scoped or zero-scope key cannot pass an allowSession… check by being "authenticated".

Edit this page on GitHub

On this page