# Adding Better Auth Plugins

> How to add and configure new Better Auth plugins to extend authentication functionality.

*Canonical: https://makerkit.dev/docs/nextjs-drizzle/better-auth/adding-plugins*

---

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](./overview).

{% callout type="note" %}
Examples below use placeholder names like `<your-plugin>`. Replace these with the actual plugin name from [Better Auth's plugin documentation](https://www.better-auth.com/docs/plugins).
{% /callout %}

## 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/`:

```typescript {% title="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:

```typescript
// Example: adding the Bearer plugin
import { bearer } from 'better-auth/plugins/bearer';

export const bearerPlugin = bearer();
```

**Factory function** - when plugin needs runtime environment values:

```typescript
// 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:

```typescript
// 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 the `plugins` array in `packages/better-auth/src/auth.ts`:

```typescript {% title="packages/better-auth/src/auth.ts" %}
import { yourPluginConfig } from './<your-plugin>';
// or for factory pattern:
import { createYourPlugin } from './<your-plugin>';

export const auth = betterAuth({
  plugins: [
    // ... 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`:

```typescript
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:

### How schema generation works

`schema:generate` runs the Better Auth CLI against a **dedicated generation config** at `packages/better-auth/src/config.ts` — not the runtime `auth.ts`. This separation is deliberate:

- `auth.ts` reads required runtime env (e.g. `NEXT_PUBLIC_SITE_URL`) at module load and fails fast when it's missing — correct for the running app, but it would crash generation, which has no such env.
- `config.ts` is **env-free**: it uses static placeholders for the secret/base URL and registers the table-contributing plugins with no required-env reads.

The set of tables in the generated `core.ts` is decided **solely by which plugins are registered in `config.ts`**. So a plugin's table is only emitted if that plugin is present there. Toggles are honoured the same way — for example, the passkey plugin (and its `passkey` table) is registered only when `ENABLE_PASSKEY` is `true`:

```typescript {% title="packages/better-auth/src/config.ts" %}
const passkeyPlugins = props.enablePasskey ? [passkey({ /* ... */ })] : [];

return betterAuth({
  // ...
  plugins: [
    organizationPlugin,
    createTwoFactorPlugin(),
    ...passkeyPlugins, // emits the `passkey` table only when enabled
    ...billingPlugins,
  ],
});
```

**When adding a table-generating plugin, register it in `config.ts` as well as `auth.ts`**, or generation will silently omit its tables.

#### 1. Generate the Better Auth schema:

```bash
pnpm --filter @kit/better-auth schema:generate
```

#### 2. Generate Drizzle migrations:

```bash
pnpm --filter @kit/database drizzle:generate
```

#### 3. Apply migrations:

```bash
pnpm --filter @kit/database drizzle:migrate
```

## Step 5: Environment Variables

If your plugin requires secrets or configuration:

#### Add variables to `.env.local`:

```bash {% title="apps/web/.env.local" %}
YOUR_PLUGIN_SECRET=your-secret-value
NEXT_PUBLIC_YOUR_PLUGIN_KEY=public-value
```

#### Validate with Zod in your plugin file:

```typescript
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:

```typescript
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](https://www.better-auth.com/docs/plugins/bearer) enables API token authentication for server-to-server requests.

### 1. Create Plugin File

```typescript {% title="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

```typescript {% title="packages/better-auth/src/auth.ts" %}
import { bearerPlugin } from './bearer';

export const auth = betterAuth({
  plugins: [
    // ... existing plugins
    bearerPlugin,
  ],
});
```

### 3. Register Client Plugin

```typescript {% title="packages/better-auth/src/auth-client.ts" %}
import { bearerClient } from 'better-auth/client/plugins';

export const authClient = createAuthClient({
  plugins: [
    // ... existing plugins
    bearerClient(),
  ],
});
```

### 4. Usage

```typescript
// 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:

| 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.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "Where can I find available Better Auth plugins?", "answer": "Check the Better Auth documentation at better-auth.com/docs/plugins for the full list of official plugins and their configuration options."},
     {"question": "Can I create custom plugins?", "answer": "Yes. Better Auth supports custom plugins. See the Better Auth documentation for the plugin API."},
     {"question": "Do all plugins need client registration?", "answer": "No. Only plugins with client-side functionality need client registration. Server-only plugins just need the server config."},
     {"question": "How do I disable an existing plugin?", "answer": "Remove it from the plugins array in packages/better-auth/src/auth.ts. Also remove it from auth-client.ts if it has a client-side companion."},
     {"question": "What if a plugin conflicts with existing functionality?", "answer": "Check the Better Auth documentation for known conflicts. Most plugins are designed to work together, but some may have overlapping features."}
   ]
/%}

---

**Next:** [Captcha Plugin →](./captcha-plugin)
