# @suaveplan/notification-channel - Multi-channel notification router with templates, sliding-window rate limiting, retry + dead-letter, scheduler, preferences, hooks, OpenTelemetry instrumentation, and Zod-validated boundaries. Universal: runs unchanged in Bun, Node, and the browser. Multi-channel notification router with templates, sliding-window rate limiting, retry + dead-letter, scheduler, preferences, hooks, OpenTelemetry instrumentation, and Zod-validated boundaries. Universal: runs unchanged in Bun, Node, and the browser. ## Documentation tree - [delivery-channel](./src/channels/delivery-channel.md) — `delivery-channel.ts` defines the structural contract every concrete delivery channel implements. The contract is inten… - [email-channel](./src/channels/email-channel.md) — `email-channel.ts` ships the `EmailChannel` `DeliveryChannel` implementation. The class is provider-neutral: the integr… - [in-app-channel](./src/channels/in-app-channel.md) — `in-app-channel.ts` ships the `InAppChannel` implementation that stores notifications for an in-app inbox / unread badg… - [push-channel](./src/channels/push-channel.md) — `push-channel.ts` ships the `PushChannel` implementation that delivers notifications via the Web Push Protocol (RFC 803… - [sms-channel](./src/channels/sms-channel.md) — `sms-channel.ts` ships the `SmsChannel` `DeliveryChannel` implementation. Like the email channel, it is provider-neutra… - [webhook-channel](./src/channels/webhook-channel.md) — `webhook-channel.ts` ships the `WebhookChannel` implementation that POSTs notifications to an arbitrary HTTPS endpoint.… - [digest](./src/digest/digest-channel.md) — `digest-channel.ts` ships the `DigestChannel` implementation that batches notifications into periodic summaries deliver… - [digest-channel](./src/digest/digest-channel.md) — `digest-channel.ts` ships the `DigestChannel` implementation that batches notifications into periodic summaries deliver… - [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` is the single source of truth for every error the `@suaveplan/notification-channel` package can throw.… - [error-codes](./src/error-codes/error-codes.md) — `error-codes.ts` is the single source of truth for every error the `@suaveplan/notification-channel` package can throw.… - [error-mapping](./src/error-mapping/error-mapping.md) — `error-mapping.ts` is the canonical place every reason code in `NOTIFICATION_CHANNEL_CODES` is paired with its typed su… - [error-mapping](./src/error-mapping/error-mapping.md) — `error-mapping.ts` is the canonical place every reason code in `NOTIFICATION_CHANNEL_CODES` is paired with its typed su… - [fakes](./src/fakes/fake-fetch.md) — `fake-fetch.ts` ships the `FakeFetch` test double — a `FetchLike` that consumes scripted responses from a FIFO queue an… - [fake-fetch](./src/fakes/fake-fetch.md) — `fake-fetch.ts` ships the `FakeFetch` test double — a `FetchLike` that consumes scripted responses from a FIFO queue an… - [hooks](./src/hooks/hooks.md) — `hooks.ts` exposes a small typed hook bus that runs before `NotificationRouter` commits a notification to a channel. Th… - [hooks](./src/hooks/hooks.md) — `hooks.ts` exposes a small typed hook bus that runs before `NotificationRouter` commits a notification to a channel. Th… - [otel](./src/otel/metrics.md) — `otel/metrics.ts` registers the package's four OpenTelemetry instruments on a meter named `@suaveplan/notification-chan… - [metrics](./src/otel/metrics.md) — `otel/metrics.ts` registers the package's four OpenTelemetry instruments on a meter named `@suaveplan/notification-chan… - [preferences](./src/preferences/preference-manager.md) — `preference-manager.ts` ships the `PreferenceManager` class that answers, at every send-time, "should this notification… - [preference-manager](./src/preferences/preference-manager.md) — `preference-manager.ts` ships the `PreferenceManager` class that answers, at every send-time, "should this notification… - [rate-limiter](./src/rate-limiter/channel-rate-limiter.md) — `channel-rate-limiter.ts` ships the `ChannelRateLimiter` class — a per-channel + per-user sliding-window rate limiter.… - [channel-rate-limiter](./src/rate-limiter/channel-rate-limiter.md) — `channel-rate-limiter.ts` ships the `ChannelRateLimiter` class — a per-channel + per-user sliding-window rate limiter.… - [retry](./src/retry/delivery-retry.md) — `delivery-retry.ts` ships the `DeliveryRetry` wrapper that retries a `DeliveryChannel.send` call with exponential backo… - [delivery-retry](./src/retry/delivery-retry.md) — `delivery-retry.ts` ships the `DeliveryRetry` wrapper that retries a `DeliveryChannel.send` call with exponential backo… - [router](./src/router/notification-router.md) — `notification-router.ts` ships the `NotificationRouter` class — the primary entrypoint a consumer talks to. It resolves… - [notification-router](./src/router/notification-router.md) — `notification-router.ts` ships the `NotificationRouter` class — the primary entrypoint a consumer talks to. It resolves… - [scheduler](./src/scheduler/notification-scheduler.md) — `notification-scheduler.ts` ships the `NotificationScheduler` class for scheduling notifications for future delivery. T… - [notification-scheduler](./src/scheduler/notification-scheduler.md) — `notification-scheduler.ts` ships the `NotificationScheduler` class for scheduling notifications for future delivery. T… - [schemas](./src/schemas/schemas.md) — `schemas.ts` exposes a Zod schema for every public option bundle in the package so callers can validate untrusted input… - [schemas](./src/schemas/schemas.md) — `schemas.ts` exposes a Zod schema for every public option bundle in the package so callers can validate untrusted input… - [templates](./src/templates/template-engine.md) — `template-engine.ts` ships the `TemplateEngine` registry that turns named templates into channel-specific rendered outp… - [template-engine](./src/templates/template-engine.md) — `template-engine.ts` ships the `TemplateEngine` registry that turns named templates into channel-specific rendered outp… ## Install ```bash bun add @suaveplan/notification-channel ``` ## Quick start ```ts import { EmailChannel, NotificationRouter, } from "@suaveplan/notification-channel"; const email = new EmailChannel({ from: "noreply@example.com", sendFn: async (msg) => ({ messageId: await provider.send(msg) }), }); const router = new NotificationRouter({ channels: [{ channel: "email", enabled: true, rateLimit: 100 }], defaultChannel: "email", deliveryChannels: [email], }); router.on("notification.failed", (n) => audit.record(n)); await router.send({ recipient: "user@example.com", body: "Welcome to Suaveplan", subject: "Welcome", }); ``` ## Surface - **Five sending channels** — `EmailChannel`, `SmsChannel`, `PushChannel`, `InAppChannel`, `WebhookChannel`. - **One aggregating channel** — `DigestChannel` (wraps any sending channel). - **Router** — `NotificationRouter` with templates, routing rules, lifecycle events, OTel meters. - **Composables** — `ChannelRateLimiter`, `DeliveryRetry`, `NotificationScheduler`, `PreferenceManager`, `TemplateEngine`. - **Hooks** — `notification:routing` pre-routing pipeline (`onNotificationRouting`). - **Errors** — stable `NOTIFICATION_CHANNEL_CODES` registry with eleven reasons. - **Test double** — `FakeFetch` for deterministic `FetchLike` injection. - **OpenTelemetry** — four instruments under the `@suaveplan/notification-channel` meter. ## Modules - [`error-codes`](./src/error-codes.md) — `NOTIFICATION_CHANNEL_CODES` + typed error subclasses. - [`hooks/hooks`](./src/hooks/hooks.md) — generic hook bus + `notification:routing` pipeline. - [`schemas/schemas`](./src/schemas/schemas.md) — Zod schemas for every public option bundle. - [`channels/delivery-channel`](./src/channels/delivery-channel.md) — common transport contract. - [`channels/email-channel`](./src/channels/email-channel.md) — email delivery. - [`channels/sms-channel`](./src/channels/sms-channel.md) — SMS delivery. - [`channels/push-channel`](./src/channels/push-channel.md) — Web Push delivery (RFC 8030). - [`channels/in-app-channel`](./src/channels/in-app-channel.md) — in-app inbox delivery. - [`channels/webhook-channel`](./src/channels/webhook-channel.md) — HTTP webhook delivery. - [`digest/digest-channel`](./src/digest/digest-channel.md) — digest / summary delivery. - [`templates/template-engine`](./src/templates/template-engine.md) — template registry + renderer. - [`rate-limiter/channel-rate-limiter`](./src/rate-limiter/channel-rate-limiter.md) — sliding-window rate limits. - [`retry/delivery-retry`](./src/retry/delivery-retry.md) — retry wrapper + dead-letter queue. - [`scheduler/notification-scheduler`](./src/scheduler/notification-scheduler.md) — future-delivery scheduler. - [`preferences/preference-manager`](./src/preferences/preference-manager.md) — opt-in/out + quiet hours. - [`router/notification-router`](./src/router/notification-router.md) — primary multi-channel router. - [`otel/metrics`](./src/otel/metrics.md) — OpenTelemetry instruments. - [`fakes/fake-fetch`](./src/fakes/fake-fetch.md) — deterministic `FetchLike` test double. ## Architecture Every dependency is injectable: `Clock` and `FetchLike` come from `@suaveplan/types`, the storage adapter behind `PreferenceManager` is caller-supplied, and every channel takes a `sendFn` callback so the package never bundles a specific provider SDK. The router uses `EventBus` so lifecycle events flow through the same typed-pub/sub primitive as the rest of the project. The 11-code `defineCodes("NOTIFICATION_CHANNEL", […])` registry preserves literal-typed unions through the public surface, so an exhaustive `switch (err.code)` reports unhandled codes at type-check time. OpenTelemetry meters fire on every router send and every dead-letter event so dashboards reflect real production behaviour out of the box. ## License MIT