Good Webhooks

Better Auth plugin

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

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.

  1. browserCall

    authClient.goodWebhooks.create({ url, eventTypes })

  2. pluginAuthorize

    Checks the session, and the user's permission for the requested scope.

  3. your databaseStore

    Saves the endpoint with an encrypted signing secret through your adapter.

  4. browserShow once

    The response carries the signing secret. No later read does.

Creating an endpoint through the plugin

The plugin manages endpoints only; it sends nothing. To send events, add the built-in delivery or a custom sender.

Want to see it work first? The Better Auth quick start runs the whole flow in one file with SQLite.

Install

npm install good-webhooks@beta better-auth@1.7.7

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

Add the server plugin

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 })], 
})
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

Use Better Auth's CLI with the version that matches your installation:

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.

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

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

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({})

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

OptionDefaultPurpose
eventTypesrequiredConfigured event names. Pass a literal tuple for typed clients
authorizeScopedenyPolicy for application scope and custom scope types. See permissions
allowLocalhostfalsePermit HTTP loopback URLs in development
schema.modelNamewebhookEndpointRename the model. Not a PostgreSQL namespace
schema.fieldsRename 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

On this page