Why I needed a notification infrastructure
Built a SaaS app last year. Users needed notifications - welcome emails, password resets, weekly digests, SMS alerts for critical events, push notifications for mobile app. Sounded straightforward.
Ended up with a mess. SendGrid for emails, Twilio for SMS, Firebase Cloud Messaging for push. Separate APIs, different formatting, duplicate logic across codebase. Managing user preferences ("don't send SMS after 10 PM") became a nightmare.
Then discovered Novu. Open-source notification infrastructure. Centralize all channels, manage preferences in one place, design templates visually. Self-hosted or cloud option. Basically, build your own notification service without the headache.
Megrated all notification logic in a weekend. One API call instead of three. User preferences unified. Templates managed in UI instead of code. Should've started with Novu from day one.
What Novu actually does
Single API for all notification channels - email, SMS, push, in-app, chat (Slack, Discord). Centralized template management with drag-and-drop editor. User preference management (opt-out per channel). Digest and batching (combine notifications). Delivery analytics and retry logic. Self-hosted or managed cloud. Open source, so you own the data.
Installation and setup
Two options: self-hosted or Novu Cloud. I'll show both.
Option 1: Self-hosted with Docker
# Clone Novu repo
git clone https://github.com/novuhq/novu.git
cd novu
# Copy environment file
cp .env.example .env
# Edit .env with your configuration
# - JWT_SECRET (generate strong secret)
# - REDIS_HOST (Redis connection)
# - MONGO_URL (MongoDB connection)
# Start with Docker Compose
docker-compose up -d
# Access web dashboard at http://localhost:4200
Option 2: Novu Cloud (managed)
# Sign up at https://dash.novu.co
# Get API key from Settings > API Keys
# Install SDK
npm install @novu/node
Initialize SDK
import { Novu } from '@novu/node';
const novu = new Novu(process.env.NOVU_API_KEY);
// Test connection
await novu.trigger('test-notification', {
to: {
subscriberId: 'user-123',
},
payload: {
message: 'Hello from Novu!',
},
});
Core concepts
Understand these and you understand Novu.
1. Subscribers
People who receive notifications. Each subscriber has channels configured (email, phone, push tokens).
await novu.subscribers.identify('user-123', {
email: 'user@example.com',
firstName: 'John',
lastName: 'Doe',
phone: '+1234567890',
locale: 'en',
data: {
avatarUrl: 'https://example.com/avatar.jpg',
},
});
// Add channels to subscriber
await novu.subscribers.updateCredentials('user-123', {
providerId: 'sendgrid',
credentials: {
email: 'user@example.com',
},
});
await novu.subscribers.updateCredentials('user-123', {
providerId: 'fcm',
credentials: {
deviceTokens: ['push-token-123'],
},
});
2. Workflows (formerly Templates)
Define how notifications look and behave. Create in dashboard or code.
// Create workflow in dashboard or via API
const workflow = await novu.workflows.create({
name: 'welcome-email',
description: 'Send welcome email to new users',
tags: ['onboarding', 'email'],
steps: [
{
type: 'email',
template: {
subject: 'Welcome to {{appName}}',
body: 'Hello {{firstName}}
Thanks for joining!
',
},
},
],
active: true,
});
3. Triggers
Execute workflows with data. This is how you send notifications from your app.
await novu.trigger('welcome-email', {
to: {
subscriberId: 'user-123',
},
payload: {
appName: 'MyApp',
firstName: 'John',
},
// Override actor (who sent notification)
actor: {
subscriberId: 'system',
},
});
4. Topics
Broadcast notifications to groups of subscribers.
// Create topic
await novu.topics.create({
key: 'product-updates',
name: 'Product Updates',
});
// Add subscribers to topic
await novu.topics.addSubscribers('product-updates', {
subscribers: ['user-123', 'user-456'],
});
// Trigger to all topic subscribers
await novu.trigger('product-update', {
to: [{ type: 'Topic', topicKey: 'product-updates' }],
payload: {
updateTitle: 'New Feature Released',
},
});
Setting up notification channels
Configure each channel in Novu dashboard. Here's what I use.
1. Email channel
// In dashboard: Integrations > Email > SendGrid
// Enter your SendGrid API key
// Or configure via code
await novu.integrations.create({
providerId: 'sendgrid',
credentials: {
apiKey: process.env.SENDGRID_API_KEY,
},
active: true,
channel: 'email',
});
// Design email template visually
// Templates > Create New > Email
// Drag-and-drop editor with variables
2. SMS channel
// In dashboard: Integrations > SMS > Twilio
// Enter Twilio Account SID and Auth Token
await novu.integrations.create({
providerId: 'twilio',
credentials: {
accountSid: process.env.TWILIO_ACCOUNT_SID,
authToken: process.env.TWILIO_AUTH_TOKEN,
},
active: true,
channel: 'sms',
});
3. Push notifications
// In dashboard: Integrations > Push > FCM/APNs
// Upload Firebase service account or APNs certificate
await novu.integrations.create({
providerId: 'fcm',
credentials: {
privateKey: process.env.FIREBASE_PRIVATE_KEY,
projectId: 'my-project',
},
active: true,
channel: 'push',
});
// Store push tokens for subscriber
await novu.subscribers.updateCredentials('user-123', {
providerId: 'fcm',
credentials: {
deviceTokens: ['push-token-from-mobile-app'],
},
});
4. In-app notifications
# Install frontend SDK
npm install @novu/react
# React component
import { NovuProvider, useNotifications } from '@novu/react';
function App() {
return (
);
}
function NotificationCenter() {
const { notifications, fetchNotifications, markAsSeen } = useNotifications();
return (
{notifications.map(notification => (
{notification.payload.message}
))}
);
}
5. Chat apps (Slack, Discord, etc.)
// In dashboard: Integrations > Chat > Slack
// OAuth with your Slack workspace
await novu.integrations.create({
providerId: 'slack',
credentials: {
webhookUrl: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL',
},
active: true,
channel: 'chat',
});
Creating notification workflows
Real workflows I use in production.
Workflow 1: Welcome email series
// Step 1: Send immediately after signup
await novu.trigger('welcome-email', {
to: { subscriberId: 'user-123' },
payload: {
firstName: 'John',
confirmationLink: 'https://myapp.com/confirm/123',
},
});
// Step 2: Onboarding tips (24 hours later)
await novu.trigger('onboarding-tips', {
to: { subscriberId: 'user-123' },
payload: {
tips: ['Tip 1', 'Tip 2', 'Tip 3'],
},
});
// Step 3: Check-in (3 days later)
await novu.trigger('welcome-checkin', {
to: { subscriberId: 'user-123' },
payload: {},
});
Workflow 2: Transactional emails with retry
// Password reset with automatic retry on failure
await novu.trigger('password-reset', {
to: { subscriberId: 'user-123' },
payload: {
resetLink: 'https://myapp.com/reset/token-123',
expiresAt: '1 hour',
},
options: {
retries: {
count: 3,
interval: 300, // 5 minutes
},
},
});
Workflow 3: Multi-channel alerts
// Critical alert: send to email, SMS, and push simultaneously
await novu.trigger('critical-alert', {
to: { subscriberId: 'user-123' },
payload: {
alertMessage: 'Server is down!',
severity: 'critical',
actionLink: 'https://myapp.com/incidents/123',
},
});
Workflow 4: Daily digest
// Batch notifications into daily digest
await novu.trigger('daily-digest', {
to: { subscriberId: 'user-123' },
payload: {
notifications: [
{ title: 'New follower', time: '2:30 PM' },
{ title: 'Comment on your post', time: '3:15 PM' },
{ title: 'Task completed', time: '4:00 PM' },
],
},
// Digest configuration
overrides: {
email: {
subject: 'Your daily digest',
},
},
});
User preferences
Let users control their notification settings.
Get subscriber preferences
const subscriber = await novu.subscribers.get('user-123');
// Preferences structure
console.log(subscriber.data.preferences);
// {
// email: true,
// sms: false,
// push: true,
// workflow_preferences: {
// 'marketing-emails': false,
// 'product-updates': true,
// }
// }
Update preferences via API
await novu.subscribers.updatePreferences('user-123', {
email: false, // Disable all emails
workflow_preferences: {
'critical-alerts': {
email: true, // Except critical alerts
sms: true,
push: true,
},
},
});
Preferences widget (React)
import { NotificationPreferences } from '@novu/react';
function UserSettings() {
return (
Notification Settings
);
}
Analytics and monitoring
Track notification performance.
Delivery status
// Get notification execution status
const execution = await novu.notifications.getExecution('execution-id-123');
console.log(execution.status);
// 'success' | 'failed' | 'pending'
console.log(execution.channels);
// {
// email: { status: 'success', providerId: 'sendgrid' },
// sms: { status: 'failed', error: 'Invalid phone number' },
// }
Analytics dashboard
# Built-in dashboard at http://localhost:4200
# View:
# - Total notifications sent
# - Delivery rate per channel
# - Error rates
# - Popular workflows
# - Subscriber engagement
Webhook events
// Novu sends webhooks for events
app.post('/webhooks/novu', (req, res) => {
const event = req.body;
switch (event.type) {
case 'notification.sent':
console.log('Notification sent:', event.data);
break;
case 'notification.failed':
console.error('Notification failed:', event.data);
// Log to error tracking
Sentry.captureException(event.data.error);
break;
case 'subscriber.created':
// Sync to your database
await syncSubscriber(event.data);
break;
}
res.status(200).send('OK');
});
Advanced features
Beyond basic notifications.
1. Digests and batching
// Combine multiple notifications into one email
await novu.trigger('activity-digest', {
to: { subscriberId: 'user-123' },
payload: {},
overrides: {
email: {
// Send digest every hour
digest: true,
strategy: 'time',
time: '60',
},
},
});
2. Delayed sending
await novu.trigger('birthday-email', {
to: { subscriberId: 'user-123' },
payload: { firstName: 'John' },
// Schedule for future date
overrides: {
email: {
delay: {
unit: 'days',
value: 30,
},
},
},
});
3. Localization
await novu.trigger('welcome-email', {
to: { subscriberId: 'user-123' },
payload: { firstName: 'John' },
// Use subscriber's locale
overrides: {
email: {
locale: 'es', // Spanish template
},
},
});
4. Tenant isolation (multi-tenant)
await novu.trigger('tenant-notification', {
to: { subscriberId: 'user-123' },
payload: {},
// Isolate by tenant
transactionId: 'tenant-456',
});
Novu vs alternatives
Quick comparison of notification services.
| Feature | Novu | SendGrid | OneSignal |
|---|---|---|---|
| Channels | Email, SMS, Push, In-app, Chat | Email only | Push, Email (limited) |
| Self-hosted | Yes | No | No |
| Open source | Yes | No | No |
| Template editor | Visual + Code | Visual + Code | Basic |
| User preferences | Built-in | Manual | Basic |
| Pricing | Free tier, then usage-based | Free tier, then tiered | Free tier, then tiered |
Common issues and fixes
Stuff that breaks and how to fix it.
Issue: Notifications not sending
Fix: Check integration credentials. Verify workflow is active. Look at execution logs in dashboard. Test with novu.trigger() and check return value.
Issue: Email templates not rendering variables
Fix: Use double curly braces: {{firstName}}. Check payload matches variable names. Test template preview in dashboard with sample data.
Issue: Push notifications not reaching devices
Fix: Verify FCM/APNs credentials. Check device tokens are valid. Ensure push notification is enabled on mobile device. Test with Novu's push notification tester.
Issue: Digests not combining
Fix: Check digest strategy is correct. Ensure same subscriber ID. Verify time window is appropriate. Look at workflow execution logs.
Issue: Self-hosted setup fails
Fix: Ensure MongoDB and Redis are running. Check environment variables. Verify Docker has enough resources. Check logs: docker-compose logs api
Best practices from production use
Hard-earned lessons.
1. Start with Novu Cloud
Self-host later if needed. Cloud has free tier and saves setup time. Migrate to self-hosted when you hit scale or compliance requirements.
2. Create workflows in dashboard
Visual template editor is faster than code. Design templates there, reference by ID in code. Better for designers too.
3. Use transactionId for idempotency
// Prevent duplicate notifications
await novu.trigger('order-confirmation', {
transactionId: `order-${orderId}`, // Unique per order
to: { subscriberId: 'user-123' },
payload: { orderId },
});
4. Monitor delivery rates
Set up alerts for high failure rates. Check analytics dashboard weekly. Clean invalid email addresses and phone numbers regularly.
5. Respect user preferences
Always check preferences before sending. Provide easy opt-out. This improves deliverability and user satisfaction.
6. Test before production
// Use test subscriber during development
const TEST_SUBSCRIBER = 'test-user';
if (process.env.NODE_ENV === 'development') {
await novu.trigger('welcome-email', {
to: { subscriberId: TEST_SUBSCRIBER },
payload: { /* test data */ },
});
}
7. Handle failures gracefully
try {
await novu.trigger('critical-alert', {
to: { subscriberId: 'user-123' },
payload: {},
});
} catch (error) {
// Log but don't crash app
console.error('Notification failed:', error);
// Store for retry later
await retryQueue.add({
workflow: 'critical-alert',
subscriberId: 'user-123',
payload: {},
});
}
Bottom line
Novu isn't for everyone. If you only send emails and have simple requirements, SendGrid might be enough. If you don't care about owning your infrastructure, use SaaS services.
But if you need multi-channel notifications, user preference management, or want to self-host, Novu is fantastic. Single API for everything, visual template editor, open-source and extensible.
Best part: started with Novu Cloud, migrated to self-hosted when we hit scale. Seamless transition, same API. No vendor lock-in.
If you're building notification infrastructure, give Novu a shot. Start with their free tier. Design a workflow. Send a test notification. See how much simpler it is than managing multiple providers yourself.