# Adding Better Auth Plugins

> How to add and configure new Better Auth plugins

*Canonical: https://makerkit.dev/docs/nextjs-prisma/better-auth/adding-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](./overview).

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](https://www.better-auth.com/docs/plugins) to see if a plugin exists for your use case.

{% 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 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/`:

```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
import { bearer } from 'better-auth/plugins/bearer';

export const bearerPlugin = bearer();
```

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

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

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

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

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

```bash {% title="./.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({ 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](https://www.better-auth.com/docs/plugins/bearer) adds API token authentication:

```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();
```

Register in `auth.ts`:

```typescript
import { bearerPlugin } from './plugins/bearer';

plugins: [
  // ... existing plugins
  bearerPlugin,
],
```

Add the client plugin in `auth-client.ts`:

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

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "Why are there two Better Auth configs (auth.ts and config.ts)?", "answer": "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."},
     {"question": "Where do I find available plugins?", "answer": "The Better Auth plugins documentation lists all official plugins with configuration options."},
     {"question": "Can I create custom plugins?", "answer": "Yes. Better Auth supports custom plugins. See the Better Auth custom plugins documentation for the plugin API."},
     {"question": "How do I disable a plugin?", "answer": "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."},
     {"question": "Do plugins affect performance?", "answer": "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 →](./auth-methods)
