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.
- browserCall
authClient.goodWebhooks.create({ url, eventTypes }) - pluginAuthorize
Checks the session, and the user's permission for the requested scope.
- your databaseStore
Saves the endpoint with an encrypted signing secret through your adapter.
- browserShow once
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 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.7Supports Better Auth >=1.7.7 <1.8.0; tested against 1.7.7.
Add the server plugin
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 })],
})export const eventTypes = ['invoice.created', 'invoice.paid'] as constKeep 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 migrationsThe 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
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
| 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 |
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.