Production-ready notifications plugin for Payload CMS with an event-driven, multi-channel architecture for email, WhatsApp, SMS, and in-app delivery.
- Event-to-rule notification dispatch through Payload jobs
- Channel implementations for email, WhatsApp, SMS, and in-app notifications
- Delivery log persistence and in-app notification storage
- Template registry and resolution utilities with starter transactional templates
- Preference mapping, consent enforcement, and custom policy hooks
- Reliability helpers for idempotency, retry classification, retry-safe dispatch behavior, and observability hooks
- Bun-based test suite, contributor guidance, CI expectations, and release-readiness docs
bun add @wtree/payload-notificationsimport { notificationsPlugin } from '@wtree/payload-notifications'
export default notificationsPlugin({
channels: ['email', 'sms', 'inapp'],
providers: {
email: {
defaultFromAddress: 'noreply@example.com',
},
sms: {
provider: 'twilio',
accountSid: process.env.TWILIO_ACCOUNT_SID,
authToken: process.env.TWILIO_AUTH_TOKEN,
from: process.env.TWILIO_SMS_FROM,
},
},
observability: {
onDispatch: async (event) => {
console.info(event)
},
},
templates: {
registry: {
'order.paid': {
email: {
subject: 'Order {{ payload.orderId }} paid',
body: 'Hi {{ userId }}, order {{ payload.orderId }} is now paid.',
},
sms: 'Order {{ payload.orderId }} is paid.',
},
},
},
})emailwhatsappsmsinapp
Templates resolve by event key and channel, and can be overridden without modifying core internals.
const config = {
templates: {
registry: {
'order.shipped': {
email: {
subject: 'Order {{ payload.orderId }} shipped',
body: 'Tracking: {{ payload.trackingNumber }}',
},
whatsapp: 'Order {{ payload.orderId }} shipped. Tracking {{ payload.trackingNumber }}.',
},
},
},
}const config = {
preferences: {
fields: {
channels: 'notificationPreferences.channels',
marketingConsent: 'notificationPreferences.marketing',
},
},
policy: {
canSend: ({ channel, classification, user }) => ({
allow: !(channel === 'sms' && classification === 'marketing' && user.plan === 'free'),
reason: 'SMS marketing disabled for free plan',
}),
},
observability: {
onDispatch: async (event) => {
console.log(event.type, event.status, event.fingerprint)
},
},
}The package exposes helpers to support retry-safe delivery and structured observability.
buildDeliveryFingerprint(input)creates a deterministic deduplication key.classifyDispatchFailure(error)marks failures asretriableorterminal.createObservabilityEvent(input)shapes structured monitoring payloads.- The send flow blocks duplicate deliveries from already-completed fingerprints.
- Retriable failures are re-queued up to three attempts.
bun test
bun run checkSee docs/testing-strategy.md for coverage expectations and CI guidance.
Starter templates are included for:
order.paidorder.shippedauth.magic-link
If you currently send notifications directly from hooks or services, migrate by:
- Emitting domain events into the plugin job pipeline.
- Moving per-channel message text into the template registry.
- Applying preference and policy checks centrally instead of inline.
- Using logs and in-app collections for auditability.
- Attaching an observability hook for metrics or external monitoring.
- Run tests and fix regressions.
- Verify provider configuration and published exports.
- Review README and examples.
- Confirm release notes and version bump.
- Validate package contents before publishing.