# Better Auth plugin

> Let signed-in users manage their own webhook endpoints through authenticated routes, stored in your existing Better Auth database.

Source: https://good-webhooks.dev/docs/better-auth



The `goodWebhooks` plugin lets signed-in users manage their own webhook endpoints in your app. It adds one model and eight routes to your Better Auth instance, and stores endpoints with your existing database adapter.

Creating an endpoint through the plugin:

1. **Call** (browser): `authClient.goodWebhooks.create({ url, eventTypes })`
2. **Authorize** (plugin): Checks the session, and the user's permission for the requested scope.
3. **Store** (your database): Saves the endpoint with an encrypted signing secret through your adapter.
4. **Show once** (browser): The response carries the signing secret. No later read does.

The plugin manages endpoints only; it sends nothing. To send events, add the [built-in delivery](https://good-webhooks.dev/docs/better-auth/delivery.md) or a [custom sender](https://good-webhooks.dev/docs/guides/custom-senders.md).

> **Note.** Want to see it work first? The [Better Auth quick start](https://good-webhooks.dev/docs/better-auth/quick-start.md) runs the
> whole flow in one file with SQLite.

### Install [#install]

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install good-webhooks@beta better-auth@1.7.7
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add good-webhooks@beta better-auth@1.7.7
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add good-webhooks@beta better-auth@1.7.7
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add good-webhooks@beta better-auth@1.7.7
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Supports Better Auth `>=1.7.7 <1.8.0`; tested against 1.7.7.

### Add the server plugin [#add-the-server-plugin]

```ts title="auth.ts"
import { betterAuth } from 'better-auth'
import { goodWebhooks } from 'good-webhooks/better-auth'
import { eventTypes } from './event-types.js'

export const auth = betterAuth({
  database, // your existing database or adapter
  secret: process.env.BETTER_AUTH_SECRET!,
  plugins: [goodWebhooks({ eventTypes })], // [!code highlight]
})
```

```ts title="event-types.ts"
export const eventTypes = ['invoice.created', 'invoice.paid'] as const
```

Keep the tuple in its own module so the browser can import it without pulling in server code. The `as const` is what carries the literal names into the client types. Management needs only names; payload validators belong to the sender.

Mount `auth.handler` the way your framework already does. The routes appear under the handler's base path, for example `POST /api/auth/good-webhooks/create`.

### Generate and apply the schema [#generate-and-apply-the-schema]

Use [Better Auth's CLI](https://better-auth.com/docs/concepts/cli) with the version that matches your installation:

```sh
npx auth@1.7.7 migrate --config ./src/auth.ts   # built-in SQL adapters
npx auth@1.7.7 generate --config ./src/auth.ts  # Drizzle or Prisma, then run their migrations
```

The plugin declares a `webhookEndpoint` model with a unique `allocationKey` index. Keep every generated field and that index; it enforces the per-scope endpoint limit under concurrent creates. MongoDB creates the index before writes.

> **Warning.** Do not apply Good Webhooks' `management.sql` here. That file is for the standalone PostgreSQL
> provider. Plugin initialization never applies migrations.

### Add the client plugin [#add-the-client-plugin]

```ts title="auth-client.ts"
import { createAuthClient } from 'better-auth/client'
import { goodWebhooksClient } from 'good-webhooks/better-auth/client'
import { eventTypes } from './event-types.js'

export const authClient = createAuthClient({
  plugins: [goodWebhooksClient({ eventTypes })],
})
```

The client entrypoint has no runtime dependency on the server plugin, a database driver, or the worker.

### Use it [#use-it]

**Browser**

```ts
const { data, error } = await authClient.goodWebhooks.create({
  url: 'https://receiver.example/webhooks',
  eventTypes: ['invoice.paid'],
})
data?.secret // show once, then never again
data?.endpoint.id

const endpoints = await authClient.goodWebhooks.list({})
```

**Server**

Server-side calls go through `auth.api` with the request's session headers:

```ts
const created = await auth.api.createWebhookEndpoint({
  headers: request.headers,
  body: { url: 'https://receiver.example/webhooks', eventTypes: ['invoice.paid'] },
})
```

An omitted `scope` means the signed-in user. Create and rotate responses include the signing secret; every other response omits it. Route responses use ISO timestamp strings.

## Options [#options]

| Option             | Default           | Purpose                                                                                               |
| ------------------ | ----------------- | ----------------------------------------------------------------------------------------------------- |
| `eventTypes`       | required          | Configured event names. Pass a literal tuple for typed clients                                        |
| `authorizeScope`   | deny              | Policy for application scope and custom scope types. See [permissions](https://good-webhooks.dev/docs/better-auth/permissions.md) |
| `allowLocalhost`   | `false`           | Permit HTTP loopback URLs in development                                                              |
| `schema.modelName` | `webhookEndpoint` | Rename the model. Not a PostgreSQL namespace                                                          |
| `schema.fields`    |                   | Rename individual fields                                                                              |

Regenerate and apply the schema after changing `schema`. Where the model lives (database, schema, collection) is decided by your Better Auth database or ORM configuration, not by the plugin.

## Next steps [#next-steps]

- [Permissions](https://good-webhooks.dev/docs/better-auth/permissions.md): Organization roles, application scope, and custom scope policies.
- [Add delivery](https://good-webhooks.dev/docs/better-auth/delivery.md): Publish and send events with the PostgreSQL engine.
- [Database adapters](https://good-webhooks.dev/docs/better-auth/database-adapters.md): Which adapters are verified and how.
- [Plugin reference](https://good-webhooks.dev/docs/reference/better-auth.md): Routes, bodies, results, and error mapping.
