Notifications

@bext-stack/framework/notifications turns one event into messages on many channels. You describe a Notification once — with a toMail, toSms, toSlack, toDatabase or toWebhook method — and notifier.send() fans it out to every channel the notification's via() lists, returning one result per channel. It's the app-developer generalisation of the Rust Mailer capability, modelled on Laravel's Notification + Channels.

When To Use It

  • Any "tell the user something" moment: a receipt, an alert, a password reset.
  • When the same event should reach more than one place (email and an in-app feed and a Slack ops channel).
  • When you want channels and their providers to be swappable and testable.

Sending

import { createNotifier, mailChannel, smsChannel, databaseChannel } from "@bext-stack/framework/notifications";

const notifier = createNotifier([
  mailChannel({ appId: "my-site" }),          // real: bext's SDK email endpoint
  smsChannel({ send: twilioSend }),           // bext ships no SMS provider — inject one
  databaseChannel({ model: Notifications }),  // in-app feed via @bext-stack/framework/orm
]);

const InvoicePaid = {
  via: () => ["mail", "database"],
  toMail: (u) => ({ subject: "Paid", html: `<p>Thanks ${u.name}</p>` }),
  toDatabase: (u) => ({ type: "invoice.paid", user_id: u.id, read: 0 }),
};

const results = await notifier.send(user, InvoicePaid);
// → { mail: { ok: true }, database: { ok: true, id: "42" } }

send() never rejects — a channel that throws or fails becomes a { ok: false, error } result without stopping the others. A channel listed in via() but not registered is reported; a channel with no matching to<Channel>() method is skipped ({ ok: true, skipped: true }).

Channels

Channel Factory Notes
Email mailChannel({ appId, deliver? }) POSTs to bext's loopback SDK email endpoint; inject deliver for tests/custom transport
SMS smsChannel({ send }) no built-in provider — you inject send (Twilio, Vonage, …). This module is the abstraction; the provider is yours
Slack slackChannel({ webhookUrl?, deliver? }) posts to an incoming webhook
In-app databaseChannel({ model }) persists a row through any create(data) — composes with the ORM for a notification feed
Webhook webhookChannel({ url?, deliver? }) POSTs a JSON payload
Custom defineChannel(name, send) build your own from one async fn

Each channel resolves its destination from the message (toMail().to) or the recipient (recipient.email / recipient.phone), so a Notification stays transport-agnostic.

Tip

bext deliberately has no SMS provider. smsChannel exists so your code depends on the abstraction, not on Twilio — swap the injected send for a fake in tests and for the real client in production without touching a single call site.

Testing

Because every channel's transport is injectable, the whole thing is unit-testable with no network — pass a deliver/send that records calls, and use a bun:sqlite-backed ORM model for the database channel.

Try It

The live notifications demo fans a message out to email + Slack (simulated) + a real persisted in-app feed — send one and watch the per-channel results and the feed update. Source in sites/demo/src/app/examples/notifications/page.tsx.

See Also