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 configs

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:

// Example: adding the Bearer plugin
import { bearer } from 'better-auth/plugins/bearer';
export const bearerPlugin = bearer();

Factory function - when plugin needs runtime environment values:

// Example: plugin that needs env config
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:

// Example: plugin enabled only when secret is configured
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 })];
}

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:generate

2. Generate Drizzle migrations:

pnpm --filter @kit/database drizzle:generate

3. Apply migrations:

pnpm --filter @kit/database drizzle:migrate

Step 5: Environment Variables

If your plugin requires secrets or configuration:

Add variables to .env.local:

apps/web/.env.local

YOUR_PLUGIN_SECRET=your-secret-value
NEXT_PUBLIC_YOUR_PLUGIN_KEY=public-value

Validate 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 token
const { data: token } = await authClient.bearer.create({
expiresIn: 60 * 60 * 24 * 30, // 30 days
});
// Use in API requests
fetch('/api/data', {
headers: {
Authorization: `Bearer ${token}`,
},
});

Existing Plugins in the Kit

The kit includes these plugins pre-configured:

PluginPurposeFile
AdminUser management, banning, impersonationadmin.ts
Magic LinkPasswordless email authenticationmagic-link.ts
OrganizationMulti-tenancy supportorganizations.ts
Two FactorTOTP-based MFAtwo-factor.ts
Email OTPEmail-based one-time passwordsotp-auth.ts
One-Time TokenVerification codes for sensitive opsone-time-token.ts
CaptchaCloudflare Turnstile bot protectioncaptcha.ts
Rate LimitBrute force protectionrate-limit.ts
Last Login MethodTracks user's last authentication methodBetter Auth built-in
BillingStripe/Polar payment integrationbilling.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?
Check the Better Auth documentation at better-auth.com/docs/plugins for the full list of official plugins and their configuration options.
Can I create custom plugins?
Yes. Better Auth supports custom plugins. See the Better Auth documentation for the plugin API.
Do all plugins need client registration?
No. Only plugins with client-side functionality need client registration. Server-only plugins just need the server config.
How do I disable an existing plugin?
Remove or comment out the plugin from the betterAuthPlugins array in plugins/index.ts. Also remove from auth-client.ts if applicable.
What if a plugin conflicts with existing functionality?
Check the Better Auth documentation for known conflicts. Most plugins are designed to work together, but some may have overlapping features.

Next: Captcha Plugin →