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.

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 configs

auth.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.ts is the runtime instance. It reads required env (e.g. NEXT_PUBLIC_SITE_URL parsed as z.url()) at module load and throws when those vars are absent. This fail-fast is intentional - don't fall back to an attacker-controlled Host header.
  • config.ts is the schema-generation entrypoint. pnpm --filter @kit/better-auth schema:generate points the Better Auth CLI at this file, not auth.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 migrations
pnpm --filter @kit/database prisma migrate dev --name add-your-plugin
# Apply migrations
pnpm --filter @kit/database prisma migrate deploy

Step 5: Environment Variables

If your plugin requires secrets:

./.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({ 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 in auth.ts) but schema:generate never emits its tables, so migrations are incomplete and auth fails with database errors. Register table-contributing plugins in both auth.ts and config.ts.
  • Gating out of sync between auth.ts and config.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.ts before adding duplicates.

Frequently Asked Questions

Why are there two Better Auth configs (auth.ts and config.ts)?
auth.ts is the runtime instance and reads required env (e.g. NEXT_PUBLIC_SITE_URL), throwing if absent. config.ts is the schema-generation entrypoint used by schema:generate; it stays env-free with static placeholders so generation never throws. Register table-contributing plugins in both, keeping any feature gating in sync.
Where do I find available plugins?
The Better Auth plugins documentation lists all official plugins with configuration options.
Can I create custom plugins?
Yes. Better Auth supports custom plugins. See the Better Auth custom plugins documentation for the plugin API.
How do I disable a plugin?
Remove it from the plugins array in auth.ts and from auth-client.ts if it has client support. Run migrations if needed to clean up unused tables.
Do plugins affect performance?
Minimally. Plugins are loaded once at startup. Runtime impact depends on the plugin - auth flow plugins add latency per request; utility plugins do not.

Next: Auth Methods →