Good Webhooks

Scopes

A scope keeps one customer's endpoints and events apart from everyone else's. It never grants access by itself.

Every endpoint and every event belongs to exactly one scope. An event only reaches endpoints in its own scope. That is how one installation serves many customers without one customer's events reaching another's endpoints.

ScopeValueTypical owner
Application scopenullYour platform itself
Named scope{ type: 'user', id }One signed-in user
Named scope{ type: 'organization', id }One organization or team
Named scope{ type: 'project', id }Any grouping your domain needs

Who receives what

An event published in one scope reaches endpoints in that scope only. There is no inheritance and no fallback:

Event published in ↓ · Endpoint in →applicationuser u_1organization o_1
application✓
user u_1✓
organization o_1✓

Application scope is not a parent of the named scopes. To reach a customer's endpoints, publish in that customer's exact scope.

Choosing a scope in code

Each API selects the scope in its own way:

The root client uses application scope. forScope() returns the same methods bound to a named scope.

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

await webhooks.publish({ type: 'invoice.paid', data }) // application scope

const customer = webhooks.forScope({ type: 'organization', id: organizationId })
await customer.endpoints.create({ url, eventTypes: ['invoice.paid'] })
await customer.publish({ type: 'invoice.paid', data }) // reaches that organization's endpoints

Scopes don't authorize

forScope() and the provider methods trust the scope you pass. Check that the caller may act for that customer before choosing the scope. Never pass an unchecked ID from a request body.

The Better Auth routes are the exception: they check the session, organization membership, and permissions on every request. The trusted provider from createBetterAuthManagement() skips those checks, which is what a worker needs.

Rules and limits

  • type is 1 to 64 characters and id is 1 to 200. Neither may be blank or contain null bytes.
  • The same id under two type values is two different scopes.
  • A scope holds at most 1,000 live endpoints.
  • Idempotency keys are unique per scope, not globally.

When an owner goes away

The Better Auth plugin retires a user's endpoints when the user is deleted, and an organization's endpoints when the organization is deleted. With standalone management, call removeScope(scope) on a createPostgresManagement() provider from your own deletion workflow. See manage endpoints.

On this page