Les applications SaaS modernes ont besoin de notifications fiables et multi-canaux — alertes in-app, emails, messages push, le tout coordonné et tracé. Construire cela de zéro signifie jongler avec des serveurs WebSocket, des fournisseurs email, des schémas de notifications et des compteurs de non-lus. Novu résout tout cela avec une seule plateforme open source.
Avec Novu v2, vous définissez les workflows de notifications directement en TypeScript, au même endroit que le code de votre application. Il n'y a pas d'interface drag-and-drop à parcourir — vos workflows sont versionnés, type-safe et testables comme n'importe quelle logique métier. Novu gère l'infrastructure : livraison en temps réel via WebSocket, fan-out multi-canaux, logique de retry et un composant React Inbox prêt à l'emploi qui s'intègre dans n'importe quelle application Next.js.
Dans ce tutoriel, vous allez construire une application de gestion des tâches avec un système de notifications complet : les utilisateurs reçoivent des alertes in-app instantanées quand des tâches leur sont assignées, avec des emails de secours s'ils sont hors ligne. Vous utiliserez le moteur workflow-as-code de Novu, un endpoint bridge et le composant React Inbox.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Un projet Next.js 15 fonctionnel avec App Router et TypeScript
- Une connaissance de base des React Server Components et des API Routes
- Un compte Novu — inscription gratuite sur novu.co (aucune carte bancaire requise)
Ce que vous allez construire
À la fin de ce tutoriel, votre application aura :
- Une cloche de notifications dans la barre de navigation avec un badge de comptage non-lus
- Un panneau Inbox Novu qui s'ouvre au clic sur la cloche, listant toutes les notifications avec support marquer-comme-lu
- Un workflow task-assigned qui envoie une alerte in-app instantanée puis un email après 10 minutes
- Une Server Action qui déclenche la notification lors de la création ou l'assignation d'une tâche
Étape 1 : Créer votre compte Novu
Rendez-vous sur novu.co et créez un compte gratuit. Après connexion :
- Créez une nouvelle Organisation (ou utilisez celle par défaut)
- Ouvrez Settings puis API Keys
- Copiez votre Secret Key (commence par
novu_secret_) - Copiez votre App Identifier (commence par
app_)
Gardez ces valeurs à portée — vous les ajouterez dans les variables d'environnement à l'étape suivante.
Étape 2 : Installer les dépendances Novu
Dans le répertoire de votre projet Next.js, installez les trois packages Novu :
npm install @novu/framework @novu/react @novu/api| Package | Rôle |
|---|---|
@novu/framework | Moteur workflow-as-code et helper pour le bridge endpoint |
@novu/react | Composant Inbox prêt à l'emploi et hook useNotifications |
@novu/api | Client API côté serveur pour déclencher les notifications |
Étape 3 : Configurer les variables d'environnement
Créez ou mettez à jour votre fichier .env.local :
# Côté serveur uniquement — ne jamais exposer au navigateur
NOVU_SECRET_KEY=novu_secret_your_key_here
# Sûr à exposer au navigateur (préfixe NEXT_PUBLIC_)
NEXT_PUBLIC_NOVU_APP_ID=app_your_identifier_here
# URL publique de votre application
NEXT_PUBLIC_APP_URL=http://localhost:3000Ajoutez les variables sans préfixe dans les secrets de votre hébergeur avant de déployer en production.
Étape 4 : Définir votre workflow de notifications
Le workflow-as-code de Novu v2 vous permet de définir la logique de notifications comme des fonctions TypeScript. Créez un fichier dédié :
// lib/novu/workflows.ts
import { workflow } from '@novu/framework';
import { z } from 'zod';
export const taskAssignedWorkflow = workflow(
'task-assigned',
async ({ step, payload }) => {
// Notification in-app instantanée
await step.inApp('in-app-alert', async () => ({
subject: `Nouvelle tâche : ${payload.taskTitle}`,
body: `${payload.assignerName} vous a assigné une tâche avec priorité ${payload.priority}.`,
}));
// Attendre 10 minutes avant d'envoyer un email
await step.delay('wait-before-email', async () => ({
amount: 10,
unit: 'minutes',
}));
await step.email('email-fallback', async () => ({
subject: `Action requise : ${payload.taskTitle}`,
body: `
<h2>Vous avez une nouvelle tâche</h2>
<p><strong>${payload.assignerName}</strong> vous a assigné :</p>
<h3>${payload.taskTitle}</h3>
<p>Priorité : ${payload.priority}</p>
<p>${payload.taskDescription}</p>
<a href="${payload.taskUrl}" style="color:#7C3AED">Voir la tâche</a>
`,
}));
},
{
payloadSchema: z.object({
taskTitle: z.string(),
taskDescription: z.string().optional().default(''),
assignerName: z.string(),
priority: z.enum(['low', 'medium', 'high']),
taskUrl: z.string().url(),
}),
}
);Points clés sur ce workflow :
- L'identifiant
'task-assigned'est ce que vous référencez lors du déclenchement step.inApp()livre une notification instantanée dans la Inbox de l'utilisateurstep.delay()met en pause l'exécution 10 minutes — Novu persiste l'état pendant la pausestep.email()s'exécute après le délai pour envoyer un email formaté- Le
payloadSchemaavec Zod valide les données de déclenchement au moment de l'exécution
Étape 5 : Créer le bridge endpoint
Novu utilise un bridge endpoint dans votre application pour découvrir et exécuter vos workflows. C'est une route API Next.js standard :
// app/api/novu/route.ts
import { serve } from '@novu/framework/next';
import { taskAssignedWorkflow } from '@/lib/novu/workflows';
export const { GET, POST, PUT } = serve({
workflows: [taskAssignedWorkflow],
});Après avoir créé ce fichier, synchronisez vos workflows avec Novu Cloud. En développement local, utilisez un tunnel pour exposer votre serveur :
# Exposer le serveur local avec ngrok
ngrok http 3000
# Synchroniser avec l'URL du tunnel
npx novu@latest sync --bridge-url https://your-ngrok-url.ngrok.io/api/novuAprès une synchronisation réussie, votre workflow task-assigned apparaît dans le tableau de bord Novu Cloud. Relancez cette commande à chaque ajout ou modification de workflows, ou automatisez-la dans votre pipeline CI/CD.
Étape 6 : Ajouter le composant Inbox
Le composant Inbox de Novu est un composant React prêt à l'emploi qui affiche une cloche avec badge et un panneau déroulant listant toutes les notifications. Ajoutez-le à votre barre de navigation :
// components/NotificationInbox.tsx
'use client';
import { NovuProvider, Inbox } from '@novu/react';
interface NotificationInboxProps {
subscriberId: string;
}
export function NotificationInbox({ subscriberId }: NotificationInboxProps) {
return (
<NovuProvider
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID!}
subscriberId={subscriberId}
>
<Inbox
appearance={{
variables: {
colorPrimary: '#7C3AED',
colorBackground: '#1F2937',
colorForeground: '#F9FAFB',
borderRadius: '0.5rem',
},
}}
/>
</NovuProvider>
);
}Le subscriberId est l'identifiant unique de votre utilisateur — généralement son ID en base de données. Novu crée automatiquement un enregistrement subscriber lors de la première notification, sans préinscription nécessaire.
Intégrez-le dans votre composant de navigation :
// components/Navbar.tsx
import { auth } from '@/lib/auth';
import { NotificationInbox } from './NotificationInbox';
export async function Navbar() {
const session = await auth();
return (
<nav className="flex items-center justify-between px-6 py-4 border-b">
<span className="text-xl font-bold">TaskFlow</span>
<div className="flex items-center gap-4">
{session?.user && (
<NotificationInbox subscriberId={session.user.id} />
)}
</div>
</nav>
);
}Étape 7 : Déclencher les notifications depuis les Server Actions
Créez un utilitaire client Novu pour usage côté serveur :
// lib/novu/client.ts
import { Novu } from '@novu/api';
export const novu = new Novu({ secretKey: process.env.NOVU_SECRET_KEY! });Déclenchez ensuite le workflow depuis une Server Action lors de l'assignation d'une tâche :
// app/actions/tasks.ts
'use server';
import { novu } from '@/lib/novu/client';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function assignTask(taskId: string, assigneeId: string) {
const session = await auth();
if (!session?.user) throw new Error('Non autorisé');
const task = await db.task.update({
where: { id: taskId },
data: { assigneeId },
include: { assignee: true },
});
// Ne pas notifier si l'utilisateur s'assigne la tâche à lui-même
if (assigneeId !== session.user.id) {
await novu.trigger({
name: 'task-assigned',
to: {
subscriberId: assigneeId,
email: task.assignee.email,
firstName: task.assignee.name ?? undefined,
},
payload: {
taskTitle: task.title,
taskDescription: task.description ?? '',
assignerName: session.user.name ?? 'Un collègue',
priority: task.priority,
taskUrl: `${process.env.NEXT_PUBLIC_APP_URL}/tasks/${taskId}`,
},
});
}
revalidatePath('/tasks');
return task;
}Étape 8 : Interface personnalisée avec le hook useNotifications
Pour un contrôle total sur l'interface des notifications, utilisez le hook useNotifications :
// components/CustomNotificationPanel.tsx
'use client';
import { useNotifications } from '@novu/react';
import { BellIcon } from 'lucide-react';
import { useState } from 'react';
export function CustomNotificationPanel() {
const [open, setOpen] = useState(false);
const {
notifications,
unreadCount,
markAllAsRead,
markAsRead,
isLoading,
} = useNotifications();
return (
<div className="relative">
<button
onClick={() => setOpen(!open)}
className="relative p-2 rounded-full hover:bg-gray-100"
>
<BellIcon className="w-5 h-5" />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">
{unreadCount}
</span>
)}
</button>
{open && (
<div className="absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-xl border z-50">
<div className="flex justify-between items-center p-4 border-b">
<span className="font-semibold">Notifications</span>
<button onClick={markAllAsRead} className="text-sm text-purple-600">
Tout marquer lu
</button>
</div>
{isLoading ? (
<div className="p-4 text-center text-gray-500">Chargement...</div>
) : notifications.length === 0 ? (
<div className="p-4 text-center text-gray-500">Aucune notification</div>
) : (
<ul className="max-h-96 overflow-y-auto">
{notifications.map((n) => (
<li
key={n.id}
onClick={() => markAsRead(n.id)}
className={`p-4 border-b cursor-pointer hover:bg-gray-50 ${
!n.isRead ? 'bg-purple-50' : ''
}`}
>
<p className="font-medium text-sm">{n.subject}</p>
<p className="text-xs text-gray-500 mt-1">{n.body}</p>
</li>
))}
</ul>
)}
</div>
)}
</div>
);
}Enveloppez ce composant dans NovuProvider exactement comme avec le composant Inbox prêt à l'emploi.
Étape 9 : Préférences de notifications
Permettez aux utilisateurs de contrôler les canaux de notifications avec le composant Preferences intégré :
// components/NotificationSettings.tsx
'use client';
import { NovuProvider, Preferences } from '@novu/react';
export function NotificationSettings({ subscriberId }: { subscriberId: string }) {
return (
<NovuProvider
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID!}
subscriberId={subscriberId}
>
<Preferences />
</NovuProvider>
);
}Affichez ce composant sur une page de paramètres utilisateur. Novu expose des bascules par workflow et par canal. Quand un utilisateur désactive les emails pour un workflow spécifique, l'étape email est silencieusement ignorée lors du prochain déclenchement — sans modification de code.
Étape 10 : Tester votre système de notifications
Test local avec Novu Local Studio
Lancez le Local Studio en parallèle de votre serveur de développement :
# Terminal 1
npm run dev
# Terminal 2
npx novu@latest devLe Local Studio s'ouvre sur http://localhost:2022 et permet de :
- Déclencher des notifications de test avec des payloads personnalisés
- Inspecter la sortie de chaque étape du workflow
- Prévisualiser les templates email avant de connecter un fournisseur
- Surveiller les logs d'exécution en temps réel
Script de test d'intégration
// scripts/test-notification.ts
import { novu } from '../lib/novu/client';
async function main() {
const result = await novu.trigger({
name: 'task-assigned',
to: {
subscriberId: 'test-user-123',
email: 'test@yourapp.com',
},
payload: {
taskTitle: "Réviser la page d'accueil",
taskDescription: "Vérifier la formulation de la section hero.",
assignerName: 'Script de test',
priority: 'medium',
taskUrl: 'http://localhost:3000/tasks/test-001',
},
});
console.log('Déclenché :', result);
}
main().catch(console.error);npx tsx scripts/test-notification.tsOuvrez votre application — la notification doit apparaître en temps réel sans rechargement de page.
Résolution des problèmes courants
La Inbox ne montre pas les notifications après déclenchement
Vérifiez que le subscriberId passé à NovuProvider correspond exactement au to.subscriberId dans l'appel trigger. Même une différence de casse crée un subscriber différent.
novu sync échoue avec une erreur de connexion
Votre URL bridge doit être accessible depuis Novu Cloud. Utilisez ngrok ou Cloudflare Tunnel pour exposer votre serveur local, puis passez l'URL publique à --bridge-url.
Les mises à jour en temps réel s'arrêtent après un rechargement
La connexion WebSocket est rétablie automatiquement. Si vous constatez un délai important, vérifiez que NEXT_PUBLIC_NOVU_APP_ID est correctement défini dans vos variables d'environnement de production.
L'étape email ne s'exécute pas en production
Connectez un fournisseur email dans le tableau de bord Novu sous Settings → Integrations (Resend, SendGrid, Postmark et d'autres sont supportés). Sans intégration email active, les étapes email sont silencieusement ignorées.
Prochaines étapes
- Notifications digest — utilisez
step.digest()pour regrouper plusieurs mises à jour en un seul email quotidien, réduisant la fatigue des notifications - Routage conditionnel — ignorez l'étape email si l'abonné a lu la notification in-app dans la fenêtre de délai
- Workflows multiples — ajoutez des workflows pour les commentaires, les rappels de délais et les mentions
- Notifications push — connectez Firebase Cloud Messaging pour étendre votre système aux appareils mobiles
- Auto-hébergement — Novu est entièrement open source et déployable avec Docker pour un contrôle total des données
Conclusion
Vous avez construit un système de notifications de niveau production avec Novu v2 et Next.js 15. Vos utilisateurs reçoivent des alertes in-app en temps réel alimentées par WebSocket, une interface Inbox soignée sans CSS à écrire, et des emails de secours pour les utilisateurs hors ligne — le tout défini en quelques dizaines de lignes de TypeScript vivant dans votre dépôt. L'approche workflow-as-code de Novu fait des notifications un citoyen de première classe de votre codebase : versionné, testable et facile à étendre à mesure que votre application grandit.