Novu: Built a Notification System That Doesn't Suck

Tired of managing notification infrastructure? Novu gives you email, SMS, push, in-app notifications in one place. Self-hosted, open source. Here's my complete integration guide.

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.