Adding Better Auth Plugins
How to add and configure new Better Auth plugins
Extend Better Auth with plugins for OAuth providers, passkeys, API tokens, and more - create a plugin file, register it, and run migrations if needed.
This page is part of the Authentication documentation.
Better Auth's plugin architecture lets you add features without modifying core auth logic. Create a plugin file in packages/better-auth/src/plugins/, import it from packages/better-auth/src/auth.ts, and optionally add a client-side plugin. Plugins that add database tables require running schema generation and migrations. The kit already includes plugins for MFA, organizations, rate limiting, and captcha - use them as patterns for adding new plugins.
A Better Auth plugin is a modular extension that adds authentication features (OAuth providers, MFA methods, session handling) without modifying the core auth configuration.
Add a plugin when: you need a feature Better Auth provides via plugin (passkeys, API tokens, new OAuth provider, custom session data).
Check the Better Auth plugins documentation to see if a plugin exists for your use case.
Examples below use placeholder names like <your-plugin>. Replace these with the actual plugin name from Better Auth's plugin documentation.
Plugin Location
packages/better-auth/└── src/ ├── auth.ts # Runtime server-side auth instance ├── config.ts # Schema-generation entrypoint (env-free) ├── auth.features.ts # Build-time feature toggles (e.g. ENABLE_PASSKEY) ├── auth-client.ts # Client-side auth instance └── plugins/ └── *.ts # Individual plugin configsauth.ts vs config.ts
There are two Better Auth configs, and you need to understand the split before adding a plugin that contributes database tables:
auth.tsis the runtime instance. It reads required env (e.g.NEXT_PUBLIC_SITE_URLparsed asz.url()) at module load and throws when those vars are absent. This fail-fast is intentional - don't fall back to an attacker-controlledHostheader.config.tsis the schema-generation entrypoint.pnpm --filter @kit/better-auth schema:generatepoints the Better Auth CLI at this file, notauth.ts, precisely because generation runs without runtime env and must not throw. It mirrors the runtime plugin set but stays env-free: required runtime values use static placeholders, while feature toggles are honoured so only the tables for enabled features are generated.
A plugin that adds tables must be registered in both files, or schema:generate won't emit its tables. Plugins with no tables only need auth.ts.
Step 1: Create Plugin File
Create a new file in packages/better-auth/src/plugins/:
packages/better-auth/src/plugins/your-plugin.ts
import { yourPlugin } from 'better-auth/plugins/<your-plugin>';/** * @name yourPluginConfig * @description What this plugin does */export const yourPluginConfig = yourPlugin({ // plugin options from Better Auth docs});Plugin Patterns
Simple plugin - direct export when no dynamic config needed:
import { bearer } from 'better-auth/plugins/bearer';export const bearerPlugin = bearer();Factory function - when plugin needs runtime environment values:
import { yourPlugin } from 'better-auth/plugins/<your-plugin>';import { env } from '@kit/shared/env';export function createYourPlugin() { return yourPlugin({ issuer: env('NEXT_PUBLIC_PRODUCT_NAME'), });}Conditional plugin - when plugin depends on optional env vars:
import * as z from 'zod';const secretKey = z.string().min(1).optional().parse(process.env.YOUR_PLUGIN_SECRET);export async function createYourPlugin() { if (!secretKey) { return [] as never; } const { yourPlugin } = await import('better-auth/plugins/<your-plugin>'); return [yourPlugin({ secretKey })];}Step 2: Register Server Plugin
Add your plugin to packages/better-auth/src/auth.ts:
import { yourPluginConfig } from './plugins/<your-plugin>';plugins: [ // ... existing plugins yourPluginConfig,],If the plugin contributes database tables, register it in packages/better-auth/src/config.ts too, so schema:generate emits those tables. Keep the gating in sync between the two files - an always-on plugin appears in both unconditionally; a toggled plugin (e.g. passkey behind ENABLE_PASSKEY) is gated by the same flag in both.
Step 3: Register Client Plugin
If the plugin has client-side functionality, add it to packages/better-auth/src/auth-client.ts:
import { yourPluginClient } from 'better-auth/client/plugins';export const authClient = createAuthClient({ plugins: [ // ... existing plugins yourPluginClient(), ],});Step 4: Database Schema (if required)
If the plugin adds database tables, first make sure it is registered in config.ts (see Step 2), then run schema generation. The schema:generate command points the Better Auth CLI at config.ts and writes the updated Prisma schema:
# Generate Better Auth schema updates (reads config.ts, writes schema.prisma)pnpm --filter @kit/better-auth schema:generate# Generate Prisma migrationspnpm --filter @kit/database prisma migrate dev --name add-your-plugin# Apply migrationspnpm --filter @kit/database prisma migrate deployStep 5: Environment Variables
If your plugin requires secrets:
./.env.local
YOUR_PLUGIN_SECRET=your-secret-valueNEXT_PUBLIC_YOUR_PLUGIN_KEY=public-valueValidate with Zod in your plugin file:
import * as z from 'zod';const pluginSecret = z .string({ message: 'YOUR_PLUGIN_SECRET is required' }) .min(32, 'Secret must be at least 32 characters') .parse(process.env.YOUR_PLUGIN_SECRET);Real-World Example: Bearer Plugin
The Bearer plugin adds API token authentication:
packages/better-auth/src/plugins/bearer.ts
import { bearer } from 'better-auth/plugins/bearer';/** * @name bearerPlugin * @description Enables Bearer token authentication for API routes */export const bearerPlugin = bearer();Register in auth.ts:
import { bearerPlugin } from './plugins/bearer';plugins: [ // ... existing plugins bearerPlugin,],Add the client plugin in auth-client.ts:
import { bearerClient } from 'better-auth/client/plugins';export const authClient = createAuthClient({ plugins: [ // ... existing plugins bearerClient(), ],});Common Pitfalls
- Forgetting to register client plugin: Server plugin works, but client-side methods are undefined. Check if the plugin has a client counterpart.
- Table-contributing plugin missing from
config.ts: It works at runtime (registered inauth.ts) butschema:generatenever emits its tables, so migrations are incomplete and auth fails with database errors. Register table-contributing plugins in bothauth.tsandconfig.ts. - Gating out of sync between
auth.tsandconfig.ts: A plugin enabled in one file but not the other generates tables you don't use, or registers a plugin whose tables don't exist. Gate both with the same flag. - Missing migrations: Plugin adds tables but you forgot to run migrations. Auth fails with database errors.
- Using sync imports for conditional plugins: Use dynamic imports (
await import()) for plugins that may not be loaded. - Not validating environment variables: Plugin silently fails or uses undefined values. Always validate with Zod.
- Registering the same plugin twice: Check
auth.tsbefore adding duplicates.
Frequently Asked Questions
Why are there two Better Auth configs (auth.ts and config.ts)?
Where do I find available plugins?
Can I create custom plugins?
How do I disable a plugin?
Do plugins affect performance?
Next: Auth Methods →