Pass onEvent to receive an event after each write that matters. Events are in-process and best-effort: a handler that throws is logged and never fails the call, and nothing is stored or retried.
Event types
ENTITLEMENTS_EVENT_TYPES lists them, and EntitlementsEvent types each one:
type |
When | subject |
data |
|---|---|---|---|
entitlements.plan.changed |
A snapshot write changes the plan the billing state resolves to, including the first snapshot of an organization that is already paying. Overrides never emit it | { type: "organization", id } |
{ from, to, status } |
entitlements.override.set |
An override is created or replaced | { type: "entitlementOverride", id } |
{ kind, key, value, expiresAt } |
entitlements.override.removed |
Staff removed an override. Expiry emits nothing | { type: "entitlementOverride", id } |
{ kind, key } |
entitlements.usage.limit_reached |
The first refusal of a metered key in a period, a limit of 0 included | { type: "organization", id } |
{ key, limit, periodEnd } |
Every event also has occurredAt, actorId (the staff user for overrides, null for syncs and usage) and organizationId, which is always set. data never holds free text, so an override’s reason stays out.
When an event fires
Events fire only for writes that are durable:
- A write outside a transaction, or on an adapter without real transactions, emits as soon as it returns.
- A write that joined your
runWithTransaction(an override write, or a sweep) queues its event with better-auth’squeueAfterTransactionHook. It is handed toonEventonce the transaction commits, and dropped if it rolls back. - An
onEventthat throws after the commit is logged and fails nothing.
Telling owners about a limit
Route limit_reached to @byo-oss/notifications. auth.api.notify exists only when that package is installed and its inbox() plugin is in plugins. Use the event’s type as the notification type, so the inbox’s throttling and preferences apply to it:
import { betterAuth } from "better-auth";
import { organization } from "better-auth/plugins/organization";
import { inbox } from "@byo-oss/notifications";
import { entitlements } from "@byo-oss/entitlements";
import { catalog } from "./catalog";
export const auth = betterAuth({
plugins: [
organization(),
inbox(),
entitlements({
catalog,
// annotate the return type so TypeScript doesn't infer `auth` from itself
onEvent: async (event): Promise<void> => {
if (event.type !== "entitlements.usage.limit_reached") return;
await auth.api.notify({
body: {
organizationId: event.organizationId,
roles: ["owner", "admin"],
type: event.type,
title: `You reached your ${event.data.key} limit`,
href: "/billing",
},
});
},
}),
],
});Several consumers can share onEvent through createEventBus from @byo-oss/core. If your app already has a notification layer, route the events through it instead of calling notify directly.