Plugins
Extend BirrJS with event handlers, endpoints, and lifecycle hooks.
Plugins let you add functionality to BirrJS without modifying its core. Each plugin has an id, optional endpoints, optional event handlers, and optional lifecycle hooks.
Plugin interface
import type { BirrJSContext } from "@birrjs/core";
interface BirrJSPlugin {
id: string;
/** HTTP endpoints merged into the BirrJS router */
endpoints?: Record<string, unknown>;
/** Called before a subscription is created */
onBeforeSubscribe?: (hookCtx: {
customerId: string;
plan: NormalizedPlan;
customerEmail?: string;
ip?: string;
}) => Promise<void>;
/** Called after checkout is ready (before redirect) */
onCheckoutReady?: (hookCtx: {
customerId: string;
planId: string;
subscriptionId: string;
checkoutUrl: string;
txRef: string;
}) => Promise<void>;
/** Event handlers for subscription lifecycle events */
onEvent?: {
"subscription.activated"?:
(payload: { customerId: string; subscriptionId: string; planId: string; startedAt: Date | null; expiresAt: Date | null },
ctx: BirrJSContext) => Promise<void> | void;
"subscription.cancelled"?:
(payload: { customerId: string; subscriptionId: string; planId: string; canceledAt: Date | null; endedAt: Date | null },
ctx: BirrJSContext) => Promise<void> | void;
"subscription.expired"?:
(payload: { customerId: string; subscriptionId: string; planId: string; expiredAt: Date },
ctx: BirrJSContext) => Promise<void> | void;
"subscription.reminder"?:
(payload: { customerId: string; subscriptionId: string; planId: string; planName: string; customerEmail: string | null; customerPhone: string | null; expiresAt: Date; daysUntilExpiry: number },
ctx: BirrJSContext) => Promise<void> | void;
/** Catch-all wildcard — fires for every event */
"*"?: (event: {
name: "subscription.activated" | "subscription.cancelled" | "subscription.expired" | "subscription.reminder";
payload: unknown;
ctx: BirrJSContext;
}) => Promise<void> | void;
};
}Lifecycle hooks
Plugins can intercept the subscription flow at two points:
| Hook | When | Can abort? |
|---|---|---|
onBeforeSubscribe | Before a subscription record is created | Yes (throw to abort) |
onCheckoutReady | After checkout URL is generated, before redirect | No (errors logged) |
const myPlugin: BirrJSPlugin = {
id: "validate-email",
onBeforeSubscribe: async (ctx) => {
if (ctx.customerEmail?.endsWith("@spam.com")) {
throw new Error("Blocked email domain");
}
},
};Event handlers
Fired automatically when BirrJS processes webhooks or cron sweeps. Plugin event handlers receive the full BirrJSContext — access to the database, logger, and queries.
| Event | When | Fired from |
|---|---|---|
subscription.activated | Payment confirmed, subscription becomes active | Webhook endpoint, callback handler |
subscription.cancelled | Subscription is cancelled | Webhook endpoint |
subscription.expired | Subscription period ends | Cron sweep |
subscription.reminder | Subscription approaching expiry | Cron sweep (daily) |
"*" | Every event (catch-all) | All of the above |
Wildcard handler
Use "*" to handle every event in one function:
const myPlugin: BirrJSPlugin = {
id: "logger",
onEvent: {
"*": async (event) => {
console.log(`Event: ${event.name}`, event.payload);
},
},
};Using the on option (user-level)
For simple reactions without building a full plugin, pass handlers directly to createBirr():
createBirr({
on: {
"subscription.activated": async (payload) => {
await myEmailService.send(payload.customerId);
},
"*": async (event) => {
console.log(event.name, event.payload);
},
},
});The difference between plugin onEvent and the user on option:
Plugin onEvent | User on option | |
|---|---|---|
| Signature | (payload, ctx) => void | (payload) => void |
| Has access to ctx | Yes (DB, logger, queries) | No |
| Best for | Plugins that need BirrJS internals | External API calls (email, SMS) |
| Wildcard signature | (event) => void with ctx | (event) => void without ctx |
Both are fired together from the same call sites — you can use on for simple handlers and plugins for complex ones simultaneously.
Endpoints
Plugins can add HTTP endpoints to the BirrJS router. These are standard better-call endpoints merged at the same base path.
import type { BirrJSPlugin } from "@birrjs/core";
const myPlugin: BirrJSPlugin = {
id: "my-plugin",
endpoints: {
"/health": myHealthEndpoint,
},
};Registering plugins
import { createBirr } from "@birrjs/core";
createBirr({
plugins: [myPlugin],
});Building a plugin
Create a function that returns a BirrJSPlugin:
import type { BirrJSPlugin, BirrJSContext } from "@birrjs/core";
interface SlackConfig {
webhookUrl: string;
}
export function slackNotifier(config: SlackConfig): BirrJSPlugin {
return {
id: "slack-notifier",
onEvent: {
"subscription.activated": async (payload, ctx) => {
const customer = await ctx.queries.getCustomer(payload.customerId);
await fetch(config.webhookUrl, {
method: "POST",
body: JSON.stringify({
text: `New subscription: ${customer?.name ?? "Unknown"} activated ${payload.planId}`,
}),
});
},
},
};
}For a complete walkthrough, see the Your First Plugin guide.