@arthurfiddich/notification-service
v1.1.1
Published
A pluggable, channel-agnostic notification service with support for in-app, push, email, SMS, and WhatsApp notifications
Maintainers
Readme
@natupicks/notification-service
A pluggable, channel-agnostic notification service for Node.js applications. Supports in-app notifications, Firebase push notifications, and is designed for easy extension to email, SMS, and WhatsApp.
Features
- Multi-channel delivery — Send to in-app, push, email, SMS, and WhatsApp from a single API
- Provider-based architecture — Register/swap providers at runtime
- Template engine —
{{variable}}interpolation with built-in e-commerce templates - Pluggable persistence — Bring your own store (MongoDB, Postgres, etc.)
- User preference checking — Respect opt-in/opt-out before sending
- Retry with exponential backoff — Automatic retries for failed deliveries
- Idempotency — Prevent duplicate notifications
- Batch sending — Send to multiple recipients efficiently
- Priority levels — Low, Normal, High, Urgent
- Zero required dependencies —
firebase-adminis an optional peer dependency
Installation
npm install @natupicks/notification-service
# If using Firebase push notifications:
npm install firebase-adminOr use as a local dependency:
{
"dependencies": {
"@natupicks/notification-service": "file:../notification-service"
}
}Quick Start
import {
NotificationService,
InAppProvider,
FirebasePushProvider,
MemoryStore,
NotificationChannel,
} from "@natupicks/notification-service";
// 1. Create a store (use MemoryStore for dev, implement NotificationStore for production)
const store = new MemoryStore();
// 2. Initialize providers
const inAppProvider = new InAppProvider();
await inAppProvider.initialize({ store });
const pushProvider = new FirebasePushProvider();
await pushProvider.initialize({
messagingInstance: firebaseAdmin.messaging(), // your firebase-admin instance
recipientResolver: {
resolvePushTokens: async (userId) => {
const user = await User.findById(userId).select("fcmTokens");
return user?.fcmTokens ?? [];
},
onInvalidPushTokens: async (userId, tokens) => {
await User.findByIdAndUpdate(userId, {
$pull: { fcmTokens: { $in: tokens } },
});
},
},
});
// 3. Create the service
const notificationService = new NotificationService({
providers: {
[NotificationChannel.IN_APP]: inAppProvider,
[NotificationChannel.PUSH]: pushProvider,
},
store,
});
// 4. Send notifications
await notificationService.send({
recipientId: userId,
channels: [NotificationChannel.IN_APP, NotificationChannel.PUSH],
templateName: "order.confirmed",
templateVars: { orderNumber: "ORD-12345" },
});Architecture
┌─────────────────────────────────────────────────────┐
│ NotificationService │
│ (orchestrator, templates, retry, preferences) │
├──────────┬──────────┬──────────┬─────────┬──────────┤
│ In-App │ Push │ Email │ SMS │ WhatsApp │
│ Provider │ Provider │ Provider │Provider │ Provider │
├──────────┴──────────┴──────────┴─────────┴──────────┤
│ NotificationStore (pluggable) │
│ Memory │ MongoDB │ Postgres │ etc. │
└─────────────────────────────────────────────────────┘Providers
InAppProvider
Stores notifications in the configured NotificationStore for in-app display.
const provider = new InAppProvider();
await provider.initialize({ store: myStore });FirebasePushProvider
Sends push notifications via Firebase Cloud Messaging.
const provider = new FirebasePushProvider();
await provider.initialize({
messagingInstance: firebaseAdmin.messaging(),
recipientResolver: myResolver,
});Custom Provider
Implement the NotificationProvider interface or extend BaseProvider:
import { BaseProvider, NotificationChannel, DeliveryStatus } from "@natupicks/notification-service";
class EmailProvider extends BaseProvider {
readonly name = "email";
async send(recipientId, payload, priority, metadata) {
// Your email sending logic (nodemailer, SES, SendGrid, etc.)
const email = await this.resolveEmail(recipientId);
await sendEmail(email, payload.title, payload.body);
return this.result(recipientId, NotificationChannel.EMAIL, DeliveryStatus.SENT);
}
}Templates
Built-in templates for common scenarios:
| Template | Variables |
|----------|-----------|
| order.confirmed | orderNumber |
| order.shipped | orderNumber, trackingNumber |
| order.delivered | orderNumber |
| order.cancelled | orderNumber |
| order.refunded | orderNumber, amount |
| promo.discount | discount, code |
| account.welcome | appName, name |
| cart.abandoned | itemCount |
| wishlist.price_drop | productName, newPrice, oldPrice |
Register custom templates:
service.registerTemplates({
name: "custom.alert",
title: "Alert: {{type}}",
body: "{{message}}",
});Notification Store
Implement NotificationStore for your database:
import type { NotificationStore } from "@natupicks/notification-service";
class MongoNotificationStore implements NotificationStore {
async save(record) { /* MongoDB insert */ }
async findById(id) { /* MongoDB findOne */ }
async findByRecipient(recipientId, options) { /* MongoDB find with pagination */ }
async getUnreadCount(recipientId) { /* MongoDB countDocuments */ }
async markAsRead(id) { /* MongoDB updateOne */ }
async markAllAsRead(recipientId) { /* MongoDB updateMany */ }
async delete(id) { /* MongoDB deleteOne */ }
async deleteAllForRecipient(recipientId) { /* MongoDB deleteMany */ }
async updateStatus(id, status) { /* MongoDB updateOne */ }
async findByIdempotencyKey(key) { /* MongoDB findOne */ }
}User Preferences
Respect user notification settings:
const service = new NotificationService({
preferenceChecker: {
isAllowed: async (recipientId, channel, category) => {
const settings = await NotificationSetting.findOne({ user: recipientId });
if (!settings) return true;
const channelSettings = settings[channel];
if (!channelSettings?.enabled) return false;
if (category && channelSettings[category] !== undefined) {
return channelSettings[category];
}
return true;
},
},
});In-App Notification Management
// Get notifications for a user
const { data, total, hasMore } = await service.getNotifications(userId, {
page: 1,
limit: 20,
unreadOnly: true,
});
// Get unread count
const count = await service.getUnreadCount(userId);
// Mark as read
await service.markAsRead(notificationId);
await service.markAllAsRead(userId);
// Delete
await service.deleteNotification(notificationId);
await service.deleteAllNotifications(userId);Configuration
const service = new NotificationService({
providers: { /* channel -> provider mapping */ },
store: myStore,
preferenceChecker: myChecker,
retry: {
maxAttempts: 3, // default: 3
baseDelay: 1000, // default: 1000ms
maxDelay: 30000, // default: 30000ms
},
logger: myLogger, // must implement Logger interface
});Roadmap
- [ ] Email provider (Nodemailer / SES / SendGrid)
- [ ] SMS provider (Twilio / SNS)
- [ ] WhatsApp provider (WhatsApp Business API)
- [ ] Scheduling support (delayed notifications)
- [ ] Notification grouping / digest
- [ ] Webhook delivery channel
- [ ] Rate limiting per user/channel
- [ ] Analytics and delivery tracking dashboard
License
MIT
