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>,
)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.
| Option | Contract |
|---|---|
eventTypes | Required configured event names. A literal tuple preserves names in server and client types. |
allowLocalhost | Optional boolean, default false. Permits development HTTP loopback destinations. |
schema.modelName | Optional physical model name, default webhookEndpoint. This is not a PostgreSQL namespace. |
schema.fields | Optional mapping from logical fields to physical field names. |
authorizeScope | Optional 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 method | POST path | Body besides scope? | Result | Action |
|---|---|---|---|---|
createWebhookEndpoint | /good-webhooks/create | url, eventTypes, description? | { endpoint, secret } | create |
listWebhookEndpoints | /good-webhooks/list | None | WebhookEndpointDTO[] | read |
getWebhookEndpoint | /good-webhooks/get | id | WebhookEndpointDTO | read |
updateWebhookEndpoint | /good-webhooks/update | id, url?, description?, eventTypes? | WebhookEndpointDTO | update |
pauseWebhookEndpoint | /good-webhooks/pause | id | WebhookEndpointDTO | update |
resumeWebhookEndpoint | /good-webhooks/resume | id | WebhookEndpointDTO | update |
removeWebhookEndpoint | /good-webhooks/remove | id | WebhookEndpointDTO | delete |
rotateWebhookEndpointSecret | /good-webhooks/rotate-secret | id, 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 scope | Authorization |
|---|---|
| Omitted | Resolves to { type: 'user', id: session.user.id }. |
user | The ID must exactly match the current user. |
organization | Requires the organization plugin, current membership, and the requested webhookEndpoint action. |
null | Application scope. Requires authorizeScope to allow the exact action. |
| Another type | Requires 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
| Condition | HTTP status |
|---|---|
| Missing session | 401 |
| Unauthorized scope or action | 403 |
INVALID_INPUT or UNSAFE_URL | 400 |
NOT_FOUND | 404 |
INVALID_STATE | 409 |
Other WebhookError | 500 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.