Novu
"Novu: notification infrastructure, multi-channel (email/SMS/push/in-app), templates, preferences, digest, workflows"
Novu is an open-source notification infrastructure layer that abstracts away individual delivery channels (email, SMS, push, in-app, chat) behind a unified API. Instead of integrating each provider separately, you define notification workflows in Novu that orchestrate multi-channel delivery with subscriber preferences, digest/batching, delays, and conditional logic. The key insight is separating notification content and routing logic from your application code: your app triggers a named workflow with dynamic data, and Novu handles template rendering, channel selection based on subscriber preferences, provider failover, and delivery tracking. Design your notifications around workflows (what to send and when) and subscribers (who receives them and how). ## Key Points - Define one workflow per notification type (e.g., "order-shipped", "new-comment"); do not overload a single workflow with conditional logic for different notification types - Use digest steps to batch high-frequency events (likes, comments, follows) into periodic summaries rather than sending individual notifications - Always provide a subscriber preference center — users who can control their notifications are less likely to disable them entirely - Use topics for group notifications instead of iterating over subscriber lists in your application code - Include an `actor` when triggering social notifications so Novu can show "User X did Y" patterns - Set up provider failover in Novu dashboard — if SendGrid is down, fall back to Amazon SES automatically - Use payload schemas (Zod) to validate trigger data at compile time and prevent runtime template errors - Test workflows with Novu's built-in trigger testing before deploying to production - **Sending notifications without subscriber preferences** — Bombarding users with unwanted notifications leads to opt-outs and uninstalls; always respect channel preferences - **Skipping the digest for high-frequency events** — Sending individual notifications for every like or reaction annoys users; batch them with digest steps - **Hardcoding notification content in application code** — Put content in Novu templates so non-developers can edit copy without code changes - **Using Novu as a real-time messaging system** — Novu is for notifications, not chat; use Stream or Ably for real-time messaging and Novu for alerting users about events
skilldb get messaging-services-skills/NovuFull skill: 355 linesNovu Notification Infrastructure
Core Philosophy
Novu is an open-source notification infrastructure layer that abstracts away individual delivery channels (email, SMS, push, in-app, chat) behind a unified API. Instead of integrating each provider separately, you define notification workflows in Novu that orchestrate multi-channel delivery with subscriber preferences, digest/batching, delays, and conditional logic. The key insight is separating notification content and routing logic from your application code: your app triggers a named workflow with dynamic data, and Novu handles template rendering, channel selection based on subscriber preferences, provider failover, and delivery tracking. Design your notifications around workflows (what to send and when) and subscribers (who receives them and how).
Setup
Installation and Client Initialization
import { Novu } from "@novu/node";
const novu = new Novu(process.env.NOVU_API_KEY!);
// Create or update a subscriber
async function createSubscriber(userId: string, userData: {
email: string;
firstName: string;
lastName: string;
phone?: string;
avatar?: string;
}) {
await novu.subscribers.identify(userId, {
email: userData.email,
firstName: userData.firstName,
lastName: userData.lastName,
phone: userData.phone,
avatar: userData.avatar,
});
}
React In-App Notification Center
import {
NovuProvider,
PopoverNotificationCenter,
NotificationBell,
} from "@novu/notification-center";
function NotificationCenter({ subscriberId }: { subscriberId: string }) {
return (
<NovuProvider
subscriberId={subscriberId}
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID!}
backendUrl="https://api.novu.co"
>
<PopoverNotificationCenter
colorScheme="light"
onNotificationClick={(notification) => {
window.location.href = notification.cta?.data?.url || "/";
}}
>
{({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
</PopoverNotificationCenter>
</NovuProvider>
);
}
Key Techniques
Triggering Workflows
// Trigger a notification workflow for a single subscriber
async function notifyOrderShipped(subscriberId: string, orderData: {
orderId: string;
trackingNumber: string;
estimatedDelivery: string;
items: { name: string; quantity: number }[];
}) {
await novu.trigger("order-shipped", {
to: { subscriberId },
payload: {
orderId: orderData.orderId,
trackingNumber: orderData.trackingNumber,
estimatedDelivery: orderData.estimatedDelivery,
items: orderData.items,
trackingUrl: `https://yourapp.com/orders/${orderData.orderId}/track`,
},
});
}
// Trigger for multiple subscribers
async function notifyTeam(memberIds: string[], announcement: {
title: string;
body: string;
authorName: string;
}) {
await novu.trigger("team-announcement", {
to: memberIds.map((id) => ({ subscriberId: id })),
payload: {
title: announcement.title,
body: announcement.body,
authorName: announcement.authorName,
},
});
}
// Trigger with actor (for "X did something" notifications)
async function notifyComment(
postAuthorId: string,
commenterId: string,
payload: { postTitle: string; commentText: string; postUrl: string }
) {
await novu.trigger("new-comment", {
to: { subscriberId: postAuthorId },
actor: { subscriberId: commenterId },
payload,
});
}
Defining Workflows with Code
import { workflow } from "@novu/framework";
import { z } from "zod";
const paymentReceivedWorkflow = workflow(
"payment-received",
async ({ step, payload }) => {
// Step 1: Send in-app notification immediately
await step.inApp("in-app-alert", async () => ({
subject: "Payment Received",
body: `Payment of $${payload.amount} received for invoice ${payload.invoiceId}.`,
avatar: "https://yourapp.com/icons/payment.png",
redirect: {
url: `/invoices/${payload.invoiceId}`,
},
}));
// Step 2: Wait 5 minutes, then send email if not seen
await step.delay("delay-before-email", async () => ({
amount: 5,
unit: "minutes",
}));
await step.email("email-receipt", async () => ({
subject: `Payment of $${payload.amount} received`,
body: `
<h1>Payment Confirmation</h1>
<p>We received your payment of <strong>$${payload.amount}</strong>.</p>
<p>Invoice: ${payload.invoiceId}</p>
<p>Date: ${payload.date}</p>
<a href="https://yourapp.com/invoices/${payload.invoiceId}">View Invoice</a>
`,
}));
},
{
payloadSchema: z.object({
amount: z.number(),
invoiceId: z.string(),
date: z.string(),
}),
}
);
Digest and Batching
const activityDigestWorkflow = workflow(
"activity-digest",
async ({ step, payload }) => {
// Collect events over a time window
const digestResult = await step.digest("batch-activities", async () => ({
amount: 30,
unit: "minutes" as const,
lookBackWindow: {
amount: 30,
unit: "minutes" as const,
},
}));
// Send a single notification summarizing all events
await step.inApp("digest-summary", async () => {
const events = digestResult.events;
const count = events.length;
return {
subject: `${count} new activities`,
body:
count === 1
? `${events[0].payload.actorName} ${events[0].payload.action}`
: `${events[0].payload.actorName} and ${count - 1} others had activity on your content.`,
};
});
await step.email("digest-email", async () => {
const events = digestResult.events;
const summaryItems = events
.map((e) => `<li>${e.payload.actorName}: ${e.payload.action}</li>`)
.join("");
return {
subject: `${events.length} new activities on your content`,
body: `<h2>Activity Summary</h2><ul>${summaryItems}</ul>`,
};
});
},
{
payloadSchema: z.object({
actorName: z.string(),
action: z.string(),
targetUrl: z.string(),
}),
}
);
Subscriber Preferences
// Get subscriber preferences
async function getPreferences(subscriberId: string) {
const { data } = await novu.subscribers.getPreference(subscriberId);
return data;
}
// Update subscriber channel preferences
async function updatePreferences(
subscriberId: string,
workflowId: string,
channels: { email?: boolean; sms?: boolean; inApp?: boolean; push?: boolean }
) {
await novu.subscribers.updatePreference(subscriberId, workflowId, {
channel: channels,
});
}
// React preference center component
import { usePreferences } from "@novu/notification-center";
function PreferenceCenter() {
const { preferences, updatePreference, loading } = usePreferences();
if (loading) return <div>Loading preferences...</div>;
return (
<div>
<h2>Notification Preferences</h2>
{preferences?.map((pref) => (
<div key={pref.template._id}>
<h3>{pref.template.name}</h3>
{pref.preference.channels &&
Object.entries(pref.preference.channels).map(([channel, enabled]) => (
<label key={channel}>
<input
type="checkbox"
checked={enabled as boolean}
onChange={(e) =>
updatePreference(pref.template._id, channel, e.target.checked)
}
/>
{channel}
</label>
))}
</div>
))}
</div>
);
}
Topic-Based Broadcasting
// Create a topic for group notifications
async function createTopic(topicKey: string, name: string) {
await novu.topics.create({ key: topicKey, name });
}
// Add subscribers to a topic
async function addSubscribersToTopic(topicKey: string, subscriberIds: string[]) {
await novu.topics.addSubscribers(topicKey, {
subscribers: subscriberIds,
});
}
// Trigger notification to an entire topic
async function notifyTopic(topicKey: string, workflowId: string, payload: Record<string, unknown>) {
await novu.trigger(workflowId, {
to: [{ type: "Topic" as const, topicKey }],
payload,
});
}
// Example: notify all members of a project
async function notifyProjectMembers(projectId: string, update: {
title: string;
description: string;
authorName: string;
}) {
await notifyTopic(`project-${projectId}`, "project-update", update);
}
Managing Notification Feed
// Server-side: query notification activity
async function getSubscriberNotifications(subscriberId: string, page: number = 0) {
const { data } = await novu.subscribers.getNotificationsFeed(subscriberId, {
page,
limit: 20,
});
return data;
}
// Mark notifications as read/seen
async function markAsRead(subscriberId: string, messageId: string) {
await novu.subscribers.markMessageAs(subscriberId, messageId, {
seen: true,
read: true,
});
}
// Mark all as read
async function markAllAsRead(subscriberId: string) {
await novu.subscribers.markAllMessagesAs(subscriberId, {
markAs: "read",
});
}
Best Practices
- Define one workflow per notification type (e.g., "order-shipped", "new-comment"); do not overload a single workflow with conditional logic for different notification types
- Use digest steps to batch high-frequency events (likes, comments, follows) into periodic summaries rather than sending individual notifications
- Always provide a subscriber preference center — users who can control their notifications are less likely to disable them entirely
- Use topics for group notifications instead of iterating over subscriber lists in your application code
- Include an
actorwhen triggering social notifications so Novu can show "User X did Y" patterns - Set up provider failover in Novu dashboard — if SendGrid is down, fall back to Amazon SES automatically
- Use payload schemas (Zod) to validate trigger data at compile time and prevent runtime template errors
- Test workflows with Novu's built-in trigger testing before deploying to production
Anti-Patterns
- Sending notifications without subscriber preferences — Bombarding users with unwanted notifications leads to opt-outs and uninstalls; always respect channel preferences
- Skipping the digest for high-frequency events — Sending individual notifications for every like or reaction annoys users; batch them with digest steps
- Hardcoding notification content in application code — Put content in Novu templates so non-developers can edit copy without code changes
- Using Novu as a real-time messaging system — Novu is for notifications, not chat; use Stream or Ably for real-time messaging and Novu for alerting users about events
- Not identifying subscribers before triggering — Triggers to non-existent subscribers silently fail; always call
subscribers.identifyduring user registration - Building custom notification feeds from scratch — Novu provides a notification center component with built-in feed management; use it instead of reimplementing read/unread tracking
- Ignoring delivery logs — Novu tracks delivery status per channel; monitor these logs to catch provider failures, invalid emails, and bounced messages early
Install this skill directly: skilldb add messaging-services-skills
Related Skills
Ably
"Ably: real-time messaging, pub/sub, presence, message history, push notifications, WebSocket/SSE"
Knock
"Knock: notification infrastructure, in-app feeds, workflows, preferences, channels, React components"
OneSignal
"OneSignal: push notifications, in-app messaging, email, SMS, segments, journeys, REST API"
Pusher
"Pusher: real-time WebSocket channels, presence channels, private channels, triggers, React hooks"
Sendbird
"Sendbird: chat SDK, group channels, open channels, messages, file sharing, moderation, UIKit components"
Stream Chat
"Stream (GetStream): chat SDK, channels, threads, reactions, typing indicators, moderation, React components"