Plugins
Bundle middleware and hooks into reusable units
A plugin bundles middleware, hooks, and lifecycle callbacks into a single reusable unit. Instead of wiring the same middleware and hooks across every client, you package them once and pass the plugin to createClient.
import { createPlugin, createClient } from '@betternotify/core';
import { withTracing, withEventLogger } from '@betternotify/core/middlewares';
const observability = createPlugin({
name: 'observability',
middleware: [
withTracing({ tracer }),
withEventLogger({ sink }),
],
hooks: {
onError: ({ route, error, phase }) => {
errorTracker.capture(error, { route, phase });
},
},
});
const mail = createClient({
catalog,
transportsByChannel: { email: transport },
plugins: [observability],
});Plugin shape
Prop
Type
How plugins compose
Plugin middleware and hooks merge with anything defined directly on the client. The ordering is:
- Middleware: plugin middleware wraps outermost → procedure
.use()wraps innermost → core. Multiple plugins compose in registration order — the first plugin is the outermost layer. - Hooks: plugin hooks run before client hooks. Within each group, handlers run in registration order.
- Lifecycle:
onCreatefires in registration order duringcreateClient.onClosefires in reverse order whenclient.close()is called.
Building a plugin
Use createPlugin to define a plugin. It gives you full autocomplete and type inference — start with what you need and add fields as the scope grows:
import { createPlugin } from '@betternotify/core';
import { withRateLimit } from '@betternotify/core/middlewares';
import type { RateLimitStore } from '@betternotify/core';
const rateLimitPlugin = (store: RateLimitStore) =>
createPlugin({
name: 'rate-limit',
middleware: [
withRateLimit({ store, key: ({ args }) => String(args.to), max: 10, window: 60_000 }),
],
});Validation with onCreate
Use onCreate to verify that the catalog meets the plugin's requirements at startup:
const auditPlugin = createPlugin({
name: 'audit',
middleware: [withEventLogger({ sink: auditSink })],
onCreate: ({ catalog }) => {
if (catalog.routes.length === 0) {
throw new Error('audit plugin requires at least one route');
}
},
});Cleanup with onClose
Use onClose to flush buffers or close connections when the client shuts down:
import { createPlugin } from '@betternotify/core';
import { withEventLogger } from '@betternotify/core/middlewares';
import type { EventSink, SendEvent } from '@betternotify/core';
const bufferPlugin = (sink: EventSink) => {
const buffer: SendEvent[] = [];
let timer: ReturnType<typeof setInterval>;
return createPlugin({
name: 'buffered-events',
middleware: [
withEventLogger({
sink: { write: async (event) => { buffer.push(event); } },
}),
],
onCreate: () => {
timer = setInterval(async () => {
const batch = buffer.splice(0);
for (const event of batch) await sink.write(event);
}, 5000);
},
onClose: async () => {
clearInterval(timer);
for (const event of buffer.splice(0)) await sink.write(event);
},
});
};Multiple plugins
Pass multiple plugins as an array. They compose in order — first plugin wraps outermost:
const mail = createClient({
catalog,
transportsByChannel: { email: transport },
plugins: [observability, rateLimitPlugin(store), auditPlugin],
});Plugin hooks and client hooks never override each other — they all run. If a plugin hook throws, the error is isolated and routed to onError just like any other hook failure. See Hooks for error isolation details.