Good Webhooks

Better Auth server

Authenticated endpoint routes and the trusted management provider.

good-webhooks/better-auth adds authenticated endpoint management routes to Better Auth and exposes the same storage as a trusted provider.

import { goodWebhooks, createBetterAuthManagement } from 'good-webhooks/better-auth'
import type {
  GoodWebhooksOptions,
  WebhookEndpointDTO,
  ManagementAction,
  ScopeAuthorization,
  ScopePolicy,
} from 'good-webhooks/better-auth'

The documented and tested baseline is Better Auth 1.7.7 on Node.js 24. The optional peer range is >=1.7.7 <1.8.0. Database support depends on the host adapter and its migration workflow. The adapter compatibility page lists tested combinations and limits. For setup steps, see the Better Auth plugin guide.

goodWebhooks

goodWebhooks<const EventTypes extends readonly string[]>(
	options: GoodWebhooksOptions<EventTypes>,
)
auth.ts
import { betterAuth } from 'better-auth'
import { goodWebhooks } from 'good-webhooks/better-auth'
import { database } from './database.js' // Your existing database or adapter.

export const auth = betterAuth({
  database,
  secret: process.env.BETTER_AUTH_SECRET!,
  plugins: [goodWebhooks({ eventTypes: ['invoice.created', 'invoice.paid'] as const })],
})

Returns the good-webhooks Better Auth server plugin. It adds one management model and eight authenticated POST routes. It does not publish events, create delivery storage, or start a worker.

OptionContract
eventTypesRequired configured event names. A literal tuple preserves names in server and client types.
allowLocalhostOptional boolean, default false. Permits development HTTP loopback destinations.
schema.modelNameOptional physical model name, default webhookEndpoint. This is not a PostgreSQL namespace.
schema.fieldsOptional mapping from logical fields to physical field names.
authorizeScopeOptional policy required for application and custom scopes. Cannot override user or organization rules.

Storage model

Schema field names are scopeKey, allocationKey, creationToken, url, description, eventTypes, additionalEventTypes, status, encryptedSecret, previousEncryptedSecret, previousSecretExpiresAt, revision, createdAt, and updatedAt.

The plugin declares a unique allocationKey index for the per-scope endpoint limit. Generated fields and indexes are part of the storage contract. The host runs Better Auth's migration or schema-generation workflow. Standalone management.sql does not create this model.

Signing secrets are encrypted with the auth instance's secret configuration. Unlike the PostgreSQL provider, this provider does not resolve hostnames at registration. The worker still checks each destination before connecting.

Authenticated routes

import { auth } from './auth.js'

export async function createEndpoint(request: Request) {
  return auth.api.createWebhookEndpoint({
    headers: request.headers,
    body: { url: 'https://receiver.example/webhooks', eventTypes: ['invoice.paid'] },
  })
}

Each route lives under Better Auth's configured base path. Every route uses sensitive session middleware and disables response storage. Server calls use auth.api.<method>({ body, headers }) with the caller's session headers. Browsers use the client plugin.

Server methodPOST pathBody besides scope?ResultAction
createWebhookEndpoint/good-webhooks/createurl, eventTypes, description?{ endpoint, secret }create
listWebhookEndpoints/good-webhooks/listNoneWebhookEndpointDTO[]read
getWebhookEndpoint/good-webhooks/getidWebhookEndpointDTOread
updateWebhookEndpoint/good-webhooks/updateid, url?, description?, eventTypes?WebhookEndpointDTOupdate
pauseWebhookEndpoint/good-webhooks/pauseidWebhookEndpointDTOupdate
resumeWebhookEndpoint/good-webhooks/resumeidWebhookEndpointDTOupdate
removeWebhookEndpoint/good-webhooks/removeidWebhookEndpointDTOdelete
rotateWebhookEndpointSecret/good-webhooks/rotate-secretid, graceMs?{ endpoint, secret }update

scope accepts { type, id }, null, or omission. Bodies reject unknown fields. Creation and updates use the management input limits. An update accepts description: null to clear the value. Rotation accepts an integer graceMs from 0 through 86,400,000, defaulting to 86,400,000.

WebhookEndpointDTO<EventType> contains id, url, nullable description, eventTypes, status, createdAt, and updatedAt. Its timestamps are ISO strings, unlike trusted provider results, which contain Date values. Ordinary route results contain no signing material. Creation and rotation return the new secret separately.

Scope authorization

Requested scopeAuthorization
OmittedResolves to { type: 'user', id: session.user.id }.
userThe ID must exactly match the current user.
organizationRequires the organization plugin, current membership, and the requested webhookEndpoint action.
nullApplication scope. Requires authorizeScope to allow the exact action.
Another typeRequires authorizeScope to allow that scope and action.
type ManagementAction = 'create' | 'read' | 'update' | 'delete'

interface ScopeAuthorization {
  scope: ManagementScope
  action: ManagementAction
  user: User
  session: Session
}

type ScopePolicy = (input: ScopeAuthorization) => boolean | Promise<boolean>
import { goodWebhooks, type ScopePolicy } from 'good-webhooks/better-auth'

const authorizeScope: ScopePolicy = ({ scope, action, user }) =>
  scope === null && action === 'read' && user.emailVerified

export const plugin = goodWebhooks({ eventTypes: ['invoice.paid'] as const, authorizeScope })

The policy defaults to denial for application and custom scopes. It does not run as an override for user or organization authorization. The permissions guide shows organization access control.

Route errors

ConditionHTTP status
Missing session401
Unauthorized scope or action403
INVALID_INPUT or UNSAFE_URL400
NOT_FOUND404
INVALID_STATE409
Other WebhookError500 with the message Webhook management failed.

Better Auth validates request bodies and handles errors outside this mapping. The Good Webhooks error code remains in mapped error bodies.

createBetterAuthManagement

createBetterAuthManagement(auth: { $context: Promise<object> }): Promise<EndpointManagement>
import { createBetterAuthManagement } from 'good-webhooks/better-auth'
import { auth } from './auth.js'

const management = await createBetterAuthManagement(auth)
await management.removeScope({ type: 'organization', id: 'org_123' })

Returns the same trusted management provider installed by goodWebhooks. A missing plugin produces INVALID_CONFIG.

This helper needs neither a session nor an HTTP listener. Direct calls bypass route authorization. It uses Better Auth's adapter and encryption helpers with the auth instance's secret configuration. App and worker must share compatible configuration, storage, and decryption keys. See add delivery to Better Auth.

The provider exposes every EndpointManagement operation, including removeScope and reencrypt, plus source for a sender. There are no HTTP routes for removeScope or reencrypt.

On this page