Adding Better Auth Plugins
How to add and configure new Better Auth plugins to extend authentication functionality.
Extend Better Auth with plugins for additional features like passkeys, API tokens, or custom authentication flows. The kit uses a modular plugin architecture that makes adding new functionality straightforward.
This page is part of the Authentication documentation.
Examples below use placeholder names like <your-plugin>. Replace these with the actual plugin name from Better Auth's plugin documentation.
Plugin Architecture
Better Auth plugins extend both server and client functionality:
- Server plugins: Add database tables, API endpoints, and server-side logic
- Client plugins: Add client methods and hooks for interacting with plugin features
Both must be configured for plugins with client-side functionality.
Plugin Location
packages/better-auth/└── src/ ├── auth.ts # Server-side auth instance ├── auth-client.ts # Client-side auth instance └── plugins/ ├── index.ts # Plugin registry └── *.ts # Individual plugin configsStep 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:
// Example: adding the Bearer pluginimport { bearer } from 'better-auth/plugins/bearer';export const bearerPlugin = bearer();Factory function - when plugin needs runtime environment values:
// Example: plugin that needs env configimport { 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:
// Example: plugin enabled only when secret is configuredimport * 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 })];}This pattern allows the app to run without the plugin when credentials aren't configured.
Step 2: Register Server Plugin
Add your plugin to packages/better-auth/src/plugins/index.ts:
import { yourPluginConfig } from './<your-plugin>';// or for factory pattern:import { createYourPlugin } from './<your-plugin>';export const betterAuthPlugins = [ // ... existing plugins adminPlugin, magicLinkPlugin, organizationPlugin, // Simple plugin yourPluginConfig, // Factory plugin createYourPlugin(), // Conditional plugin (spread array) ...(await createYourPlugin()),];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 twoFactorClient({ /* ... */ }), emailOTPClient(), // Add your client plugin yourPluginClient(), ],});Step 4: Database Schema
If the plugin adds database tables:
1. Generate the Better Auth schema:
pnpm --filter @kit/better-auth schema:generate2. Generate Drizzle migrations:
pnpm --filter @kit/database drizzle:generate3. Apply migrations:
pnpm --filter @kit/database drizzle:migrateStep 5: Environment Variables
If your plugin requires secrets or configuration:
Add variables to .env.local:
apps/web/.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({ error: 'YOUR_PLUGIN_SECRET is required' }) .min(32, 'Secret must be at least 32 characters') .parse(process.env.YOUR_PLUGIN_SECRET);Email Handlers
Plugins that send emails should use dynamic imports to avoid circular dependencies:
export const yourPluginConfig = yourPlugin({ sendEmail: async ({ user, url }) => { const { sendYourPluginEmail } = await import('../emails/send-your-plugin-email'); await sendYourPluginEmail({ email: user.email, url, productName: getProductName(), }); },});Real-World Example: Adding the Bearer Plugin
The Bearer plugin enables API token authentication for server-to-server requests.
1. Create Plugin File
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();2. Register Server Plugin
packages/better-auth/src/plugins/index.ts
import { bearerPlugin } from './bearer';export const betterAuthPlugins = [ // ... existing plugins bearerPlugin,];3. Register Client Plugin
packages/better-auth/src/auth-client.ts
import { bearerClient } from 'better-auth/client/plugins';export const authClient = createAuthClient({ plugins: [ // ... existing plugins bearerClient(), ],});4. Usage
// Generate a bearer tokenconst { data: token } = await authClient.bearer.create({ expiresIn: 60 * 60 * 24 * 30, // 30 days});// Use in API requestsfetch('/api/data', { headers: { Authorization: `Bearer ${token}`, },});Existing Plugins in the Kit
The kit includes these plugins pre-configured:
| Plugin | Purpose | File |
|---|---|---|
| Admin | User management, banning, impersonation | admin.ts |
| Magic Link | Passwordless email authentication | magic-link.ts |
| Organization | Multi-tenancy support | organizations.ts |
| Two Factor | TOTP-based MFA | two-factor.ts |
| Email OTP | Email-based one-time passwords | otp-auth.ts |
| One-Time Token | Verification codes for sensitive ops | one-time-token.ts |
| Captcha | Cloudflare Turnstile bot protection | captcha.ts |
| Rate Limit | Brute force protection | rate-limit.ts |
| Last Login Method | Tracks user's last authentication method | Better Auth built-in |
| Billing | Stripe/Polar payment integration | billing.ts |
Common Pitfalls
- Forgetting client plugin: Server-only plugins work, but you won't have client methods without the client plugin.
- Missing migrations: Plugins with database tables require migration generation and application.
- Import order issues: Use dynamic imports for email handlers to avoid circular dependencies.
- Not spreading conditional plugins: Use
...(await createPlugin())to properly spread the array.
Frequently Asked Questions
Where can I find available Better Auth plugins?
Can I create custom plugins?
Do all plugins need client registration?
How do I disable an existing plugin?
What if a plugin conflicts with existing functionality?
Next: Captcha Plugin →