Construire des fonctions durables et des workflows pilotés par événements avec Inngest et Next.js

Les applications modernes sont construites sur des événements — un utilisateur crée un compte, un paiement réussit, un fichier est téléchargé, un modèle IA termine son traitement. Chaque événement déclenche un travail qui doit être fiable : envoyer un email de confirmation, mettre à jour les enregistrements de facturation, redimensionner des images ou lancer un pipeline multi-étapes. Mais câbler tout cela soi-même avec des files de messages, des workers et une logique de retentative est pénible.
Inngest adopte une approche différente. Au lieu de gérer une infrastructure, vous écrivez des fonctions durables — des fonctions TypeScript qui survivent automatiquement aux pannes, aux retentatives et aux timeouts serverless. Chaque fonction est déclenchée par des événements et composée d'étapes qui s'exécutent exactement une fois, même si la fonction est interrompue et réexécutée. Pas de files de messages à gérer, pas de workers à déployer, pas d'instances Redis à surveiller.
Dans ce tutoriel, vous allez construire un pipeline d'onboarding et de facturation SaaS — quand un utilisateur s'inscrit, Inngest orchestre un workflow multi-étapes qui provisionne son compte, envoie un email de bienvenue, démarre un compteur d'essai et gère les événements de facturation en aval. Vous apprendrez les étapes durables, les patterns de fan-out, la coordination d'événements, les fonctions planifiées et le throttling.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Des connaissances de base en React et TypeScript
- Une familiarité avec Next.js App Router (routes API, Server Actions)
- Un éditeur de code (VS Code recommandé)
Aucun compte Inngest n'est requis pour le développement local — le serveur de développement Inngest tourne entièrement sur votre machine.
Ce que vous allez construire
Un pipeline d'onboarding et de facturation SaaS comprenant :
- Fonction de provisionnement de compte — crée les enregistrements en base et configure les paramètres par défaut lors de l'inscription
- Fonction d'email de bienvenue — envoie un email d'onboarding formaté avec logique de retentative
- Workflow de gestion d'essai — démarre un essai de 14 jours et envoie des rappels aux jours 7 et 13
- Pattern de fan-out — déclenche plusieurs fonctions indépendantes à partir d'un seul événement
- Coordination d'événements — attend un événement
billing/subscription.createdavant de continuer - Fonction planifiée — rapport quotidien de risque de churn pour l'équipe
- Throttling et concurrence — empêche de surcharger les API externes
- Observabilité locale complète — tracez chaque étape dans le tableau de bord du serveur de développement
Étape 1 : Créer le projet Next.js
Créez un nouveau projet Next.js 15 :
npx create-next-app@latest inngest-saas --typescript --tailwind --eslint --app --src-dir --use-npm
cd inngest-saasInstallez Inngest :
npm install inngestC'est la seule dépendance dont vous avez besoin. Inngest n'a aucune dépendance pair et fonctionne avec n'importe quel runtime Node.js.
Étape 2 : Initialiser le client Inngest
Créez le client Inngest partagé que toutes vos fonctions utiliseront.
// src/inngest/client.ts
import { Inngest } from "inngest";
export const inngest = new Inngest({
id: "inngest-saas",
});L'id identifie votre application. En production, toutes les fonctions enregistrées sous cet ID apparaissent ensemble dans le tableau de bord Inngest.
Vous pouvez également définir vos types d'événements pour une sécurité de typage complète :
// src/inngest/client.ts
import { Inngest } from "inngest";
type Events = {
"user/signup.completed": {
data: {
userId: string;
email: string;
name: string;
plan: "free" | "pro" | "enterprise";
};
};
"billing/subscription.created": {
data: {
userId: string;
subscriptionId: string;
plan: "pro" | "enterprise";
};
};
"user/trial.started": {
data: {
userId: string;
trialEndDate: string;
};
};
};
export const inngest = new Inngest({
id: "inngest-saas",
schemas: new EventSchemas().fromRecord<Events>(),
});Avec cette configuration, chaque appel inngest.send() et chaque déclencheur de fonction est entièrement typé — vous obtenez l'autocomplétion sur les noms d'événements et des vérifications à la compilation sur les payloads.
Étape 3 : Créer la route API
Inngest fonctionne en enregistrant un seul endpoint HTTP dans votre application. Le serveur de développement Inngest (et le service cloud en production) appelle cet endpoint pour invoquer vos fonctions.
// src/app/api/inngest/route.ts
import { serve } from "inngest/next";
import { inngest } from "@/inngest/client";
import { provisionAccount } from "@/inngest/functions/provision-account";
import { sendWelcomeEmail } from "@/inngest/functions/send-welcome-email";
import { manageTrialWorkflow } from "@/inngest/functions/manage-trial";
import { dailyChurnReport } from "@/inngest/functions/daily-churn-report";
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [
provisionAccount,
sendWelcomeEmail,
manageTrialWorkflow,
dailyChurnReport,
],
});Chaque fonction que vous écrivez est importée ici et passée à serve(). Inngest gère le routage, l'invocation et les retentatives à travers ce seul endpoint.
Étape 4 : Construire la fonction de provisionnement de compte
Votre première fonction durable. Quand un utilisateur s'inscrit, cette fonction crée les enregistrements en base de données et configure son espace de travail.
// src/inngest/functions/provision-account.ts
import { inngest } from "@/inngest/client";
export const provisionAccount = inngest.createFunction(
{
id: "provision-account",
retries: 3,
},
{ event: "user/signup.completed" },
async ({ event, step }) => {
// Étape 1 : Créer l'enregistrement utilisateur en base
const user = await step.run("create-user-record", async () => {
console.log(`Creating user record for ${event.data.email}`);
return {
id: event.data.userId,
email: event.data.email,
name: event.data.name,
plan: event.data.plan,
createdAt: new Date().toISOString(),
};
});
// Étape 2 : Créer l'espace de travail par défaut
const workspace = await step.run("create-workspace", async () => {
console.log(`Creating workspace for user ${user.id}`);
return {
workspaceId: `ws_${user.id}`,
name: `${user.name}'s Workspace`,
};
});
// Étape 3 : Initialiser les paramètres par défaut
await step.run("seed-defaults", async () => {
console.log(`Seeding defaults for workspace ${workspace.workspaceId}`);
return { seeded: true };
});
// Étape 4 : Envoyer l'événement de début d'essai
await step.sendEvent("start-trial", {
name: "user/trial.started",
data: {
userId: user.id,
trialEndDate: new Date(
Date.now() + 14 * 24 * 60 * 60 * 1000
).toISOString(),
},
});
return { user, workspace, status: "provisioned" };
}
);Pourquoi les étapes sont importantes
Chaque appel step.run() est un point de contrôle. Si la fonction plante après l'étape 2, Inngest la réinvoquera, mais les étapes 1 et 2 ne seront pas réexécutées — leurs résultats sont mémorisés. C'est ce qui rend la fonction durable. Vous obtenez une sémantique d'exécution exactement-une-fois pour chaque étape sans infrastructure supplémentaire.
Propriétés clés des étapes :
- Mémorisées — une fois une étape terminée, sa valeur de retour est mise en cache et réutilisée lors des retentatives
- Réessayables individuellement — si l'étape 3 échoue, seule l'étape 3 est retentée
- Sérialisables — les résultats des étapes doivent être sérialisables en JSON
Étape 5 : Construire la fonction d'email de bienvenue
Cette fonction se déclenche aussi sur user/signup.completed — le pattern de fan-out d'Inngest signifie que plusieurs fonctions peuvent écouter le même événement.
// src/inngest/functions/send-welcome-email.ts
import { inngest } from "@/inngest/client";
export const sendWelcomeEmail = inngest.createFunction(
{
id: "send-welcome-email",
retries: 5,
throttle: {
limit: 10,
period: "1m",
},
},
{ event: "user/signup.completed" },
async ({ event, step }) => {
// Étape 1 : Rendre le template d'email
const emailHtml = await step.run("render-template", async () => {
return renderWelcomeTemplate({
name: event.data.name,
plan: event.data.plan,
});
});
// Étape 2 : Envoyer via le fournisseur d'email
const result = await step.run("send-email", async () => {
console.log(`Sending welcome email to ${event.data.email}`);
return {
messageId: `msg_${Date.now()}`,
to: event.data.email,
status: "sent",
};
});
// Étape 3 : Logger l'événement analytics
await step.run("track-analytics", async () => {
console.log(`Tracking welcome email sent for ${event.data.userId}`);
});
return result;
}
);
function renderWelcomeTemplate(data: { name: string; plan: string }) {
return `
<h1>Bienvenue sur SaaS App, ${data.name} !</h1>
<p>Vous êtes sur le plan <strong>${data.plan}</strong>.</p>
<p>Votre essai de 14 jours commence maintenant. Explorez toutes les fonctionnalités !</p>
`;
}Throttling
La configuration throttle limite cette fonction à 10 exécutions par minute. C'est crucial lors de l'envoi d'emails — vous ne voulez pas surcharger l'API de votre fournisseur d'email ou déclencher des limites de débit lors d'un pic de trafic.
Étape 6 : Construire le workflow de gestion d'essai
C'est ici qu'Inngest brille vraiment — un workflow de longue durée qui s'étend sur des jours.
// src/inngest/functions/manage-trial.ts
import { inngest } from "@/inngest/client";
export const manageTrialWorkflow = inngest.createFunction(
{
id: "manage-trial-workflow",
retries: 3,
cancelOn: [
{
event: "billing/subscription.created",
match: "data.userId",
},
],
},
{ event: "user/trial.started" },
async ({ event, step }) => {
const userId = event.data.userId;
const trialEndDate = new Date(event.data.trialEndDate);
// Étape 1 : Attendre 7 jours puis envoyer l'email de mi-essai
await step.sleep("wait-for-day-7", "7 days");
await step.run("send-mid-trial-email", async () => {
console.log(`Sending mid-trial reminder to user ${userId}`);
return { sent: true, type: "mid-trial" };
});
// Étape 2 : Attendre jusqu'au jour 13 (6 jours de plus)
await step.sleep("wait-for-day-13", "6 days");
await step.run("send-trial-ending-email", async () => {
console.log(`Sending trial-ending warning to user ${userId}`);
return { sent: true, type: "trial-ending" };
});
// Étape 3 : Attendre le dernier jour
await step.sleep("wait-for-trial-end", "1 day");
// Étape 4 : Vérifier si l'utilisateur a converti
const hasSubscription = await step.run("check-subscription", async () => {
console.log(`Checking if user ${userId} has converted`);
return false;
});
if (!hasSubscription) {
// Étape 5 : Rétrograder au plan gratuit
await step.run("downgrade-to-free", async () => {
console.log(`Downgrading user ${userId} to free plan`);
return { plan: "free" };
});
await step.run("send-downgrade-email", async () => {
console.log(`Sending downgrade notification to user ${userId}`);
return { sent: true, type: "downgraded" };
});
}
return { userId, converted: hasSubscription };
}
);Concepts clés
step.sleep() — Met la fonction en pause pour la durée spécifiée. La fonction ne consomme pas de ressources de calcul pendant le sommeil. Inngest la réveille après la période et reprend à l'étape suivante avec tous les résultats des étapes précédentes intacts.
cancelOn — Si un événement billing/subscription.created arrive avec un userId correspondant, Inngest annule automatiquement ce workflow. L'utilisateur a converti — pas besoin d'envoyer des emails de fin d'essai ou de le rétrograder.
Cette unique fonction remplace ce qui nécessiterait autrement un cron job, une file de messages, une machine à états et une table de base de données pour suivre le statut de l'essai.
Étape 7 : Construire le rapport de churn planifié
Inngest supporte la planification basée sur cron pour les tâches récurrentes.
// src/inngest/functions/daily-churn-report.ts
import { inngest } from "@/inngest/client";
export const dailyChurnReport = inngest.createFunction(
{
id: "daily-churn-report",
},
{ cron: "0 9 * * *" }, // Chaque jour à 9h00
async ({ step }) => {
// Étape 1 : Requêter les utilisateurs à risque
const atRiskUsers = await step.run("query-at-risk-users", async () => {
console.log("Querying users with trials ending in 3 days...");
return [
{ userId: "user_1", email: "alice@example.com", trialEndsIn: 2 },
{ userId: "user_2", email: "bob@example.com", trialEndsIn: 1 },
];
});
if (atRiskUsers.length === 0) {
return { report: "Aucun utilisateur à risque aujourd'hui" };
}
// Étape 2 : Compiler le rapport
const report = await step.run("compile-report", async () => {
return {
date: new Date().toISOString(),
atRiskCount: atRiskUsers.length,
users: atRiskUsers,
summary: `${atRiskUsers.length} utilisateurs à risque de churn`,
};
});
// Étape 3 : Envoyer sur Slack
await step.run("notify-team", async () => {
console.log(`Sending churn report to Slack: ${report.summary}`);
return { notified: true };
});
return report;
}
);Le déclencheur cron remplace le déclencheur événementiel. Cette fonction s'exécute chaque jour à 9h indépendamment de tout événement. Les étapes fournissent toujours la durabilité — si la notification Slack échoue, seule cette étape est retentée.
Étape 8 : Configurer le serveur de développement
Lancez le serveur de développement Inngest à côté de votre application Next.js :
npx inngest-cli@latest devDans un terminal séparé, lancez votre application Next.js :
npm run devLe serveur de développement tourne sur http://localhost:8288 et découvre automatiquement vos fonctions en appelant votre endpoint /api/inngest. Ouvrez le tableau de bord du serveur de développement pour voir toutes les fonctions enregistrées, envoyer des événements de test et tracer les exécutions étape par étape.
Étape 9 : Déclencher des événements depuis votre application
Maintenant connectez votre flux d'inscription pour envoyer des événements à Inngest.
// src/app/api/auth/signup/route.ts
import { NextResponse } from "next/server";
import { inngest } from "@/inngest/client";
export async function POST(request: Request) {
const body = await request.json();
const { email, name, plan } = body;
const userId = `user_${Date.now()}`;
// Envoyer l'événement d'inscription — Inngest gère le reste
await inngest.send({
name: "user/signup.completed",
data: {
userId,
email,
name,
plan: plan || "free",
},
});
return NextResponse.json({ userId, status: "created" });
}Quand cet événement se déclenche, les deux fonctions provisionAccount et sendWelcomeEmail s'exécutent simultanément — fan-out sans aucune configuration. La fonction de provisionnement émet ensuite user/trial.started, qui déclenche manageTrialWorkflow. Un événement, trois fonctions, pipeline complet.
Étape 10 : Coordination d'événements avec step.waitForEvent()
Parfois vous avez besoin qu'une fonction se mette en pause et attende un événement externe avant de continuer. C'est la coordination d'événements — une alternative puissante au polling.
// src/inngest/functions/onboarding-with-verification.ts
import { inngest } from "@/inngest/client";
export const onboardingWithVerification = inngest.createFunction(
{
id: "onboarding-with-verification",
},
{ event: "user/signup.completed" },
async ({ event, step }) => {
// Étape 1 : Envoyer l'email de vérification
await step.run("send-verification-email", async () => {
console.log(`Sending verification email to ${event.data.email}`);
return { sent: true };
});
// Étape 2 : Attendre jusqu'à 24 heures que l'utilisateur vérifie
const verificationEvent = await step.waitForEvent(
"wait-for-email-verification",
{
event: "user/email.verified",
match: "data.userId",
timeout: "24h",
}
);
if (!verificationEvent) {
// Timeout — l'utilisateur n'a jamais vérifié
await step.run("send-reminder", async () => {
console.log(`User ${event.data.userId} did not verify — sending reminder`);
return { reminded: true };
});
return { status: "unverified", reminded: true };
}
// L'utilisateur a vérifié — continuer l'onboarding
await step.run("activate-full-access", async () => {
console.log(`User ${event.data.userId} verified — activating full access`);
return { activated: true };
});
return { status: "verified", activated: true };
}
);step.waitForEvent() met la fonction en pause jusqu'à ce qu'un événement correspondant arrive ou que le timeout expire. Le champ match garantit qu'elle ne reprend que lorsque le userId dans l'événement entrant correspond à l'événement d'inscription original.
Étape 11 : Contrôle de concurrence
Lors du traitement d'événements à grande échelle, vous devez souvent limiter le nombre d'exécutions simultanées — par exemple, pour respecter les limites de débit des API.
// src/inngest/functions/sync-to-crm.ts
import { inngest } from "@/inngest/client";
export const syncToCRM = inngest.createFunction(
{
id: "sync-to-crm",
concurrency: {
limit: 5,
key: "event.data.crmProvider",
},
retries: 10,
backoff: {
type: "exponential",
minDelay: "1s",
maxDelay: "5m",
},
},
{ event: "user/signup.completed" },
async ({ event, step }) => {
await step.run("push-to-crm", async () => {
console.log(`Syncing user ${event.data.userId} to CRM`);
return { synced: true };
});
}
);La configuration concurrency garantit un maximum de 5 exécutions simultanées par fournisseur CRM. Combiné avec le backoff exponentiel sur les retentatives, cette configuration gère les limites de débit et les pannes transitoires avec élégance.
Étape 12 : Tester vos fonctions
Les fonctions Inngest sont du TypeScript standard — vous pouvez les tester unitairement en mockant les outils d'étape.
// src/inngest/functions/__tests__/provision-account.test.ts
import { describe, it, expect } from "vitest";
describe("provisionAccount", () => {
it("should create user and workspace", async () => {
const mockEvent = {
data: {
userId: "test_user_1",
email: "test@example.com",
name: "Test User",
plan: "free" as const,
},
};
const user = {
id: mockEvent.data.userId,
email: mockEvent.data.email,
name: mockEvent.data.name,
plan: mockEvent.data.plan,
createdAt: expect.any(String),
};
expect(user.email).toBe("test@example.com");
expect(user.plan).toBe("free");
});
});Pour les tests d'intégration, le serveur de développement Inngest fournit un mode test où vous pouvez envoyer des événements et vérifier les sorties des fonctions de manière programmatique.
Étape 13 : Déployer en production
Option A : Inngest Cloud (recommandé)
- Créez un compte sur le site Inngest
- Obtenez votre clé de signature et clé d'événement depuis le tableau de bord
- Ajoutez les variables d'environnement :
# .env.production
INNGEST_SIGNING_KEY=signkey-prod-xxxxx
INNGEST_EVENT_KEY=eventkey-xxxxx- Déployez votre application Next.js sur Vercel, Railway ou toute autre plateforme
- Enregistrez l'URL de votre application dans le tableau de bord Inngest
Option B : Auto-hébergé
Inngest est open source. Vous pouvez exécuter le serveur Inngest vous-même :
docker run -p 8288:8288 inngest/inngest:latestDéfinissez la variable d'environnement INNGEST_BASE_URL pour pointer votre application vers votre instance auto-hébergée.
Étape 14 : Surveiller et déboguer
Le tableau de bord Inngest (cloud et serveur de développement) fournit :
- Liste des fonctions — toutes les fonctions enregistrées avec leurs déclencheurs et configuration
- Historique d'exécution — chaque exécution avec les événements d'entrée, les sorties d'étapes et le timing
- Traces d'étapes — chronologie visuelle de chaque étape au sein d'une exécution
- Détails des erreurs — traces de pile complètes avec historique de retentative
- Journal des événements — chaque événement envoyé au système avec les détails du payload
Comparaison d'Inngest avec les alternatives
| Fonctionnalité | Inngest | File traditionnelle (BullMQ) | Cron Jobs |
|---|---|---|---|
| Étapes durables | Oui | Manuel | Non |
| Piloté par événements | Oui | Routage manuel | Non |
| Sommeil de plusieurs jours | Oui (sans coût de calcul) | Nécessite un worker persistant | Hacks de timer |
| Retentative avec backoff | Intégré | Configuration manuelle | Manuel |
| Fan-out | Automatique | Routage manuel | N/A |
| Développement local | Serveur de dev avec UI | Redis requis | crontab |
| Sécurité de typage | TypeScript complet | Partiel | Aucune |
| Observabilité | Tableau de bord intégré | Outils externes | Fichiers de log |
Dépannage
Les fonctions n'apparaissent pas dans le serveur de développement
Assurez-vous que votre application Next.js tourne et que la route /api/inngest est accessible. Le serveur de développement découvre les fonctions en appelant GET /api/inngest. Vérifiez que toutes les fonctions sont importées et passées à serve().
Les étapes se réexécutent de manière inattendue
Les résultats des étapes sont mémorisés par leur identifiant d'étape (le premier argument de step.run()). Si vous changez un identifiant d'étape, Inngest le traite comme une nouvelle étape. Gardez les identifiants d'étapes stables entre les déploiements.
Le sommeil ne se réveille pas en développement
Le serveur de développement Inngest traite les sommeils en temps accéléré pour les tests. Si un sommeil semble bloqué, vérifiez les logs du serveur de développement.
Prochaines étapes
- Ajouter du middleware — Inngest supporte le middleware de fonction pour le logging, l'authentification et le reporting d'erreurs
- Traitement par lots — utilisez
step.run()dans une boucle pour traiter des tableaux d'éléments - Concurrence multi-tenant — utilisez les clés de concurrence pour isoler les limites de débit par client
- Connecter des services réels — intégrez Resend pour les emails, Stripe pour la facturation et PostHog pour les analytics
- Workflows IA — Inngest est populaire pour orchestrer des pipelines IA multi-étapes avec des appels LLM, des embeddings et la recherche vectorielle
Conclusion
Vous avez construit un pipeline complet piloté par événements pour une application SaaS avec Inngest et Next.js. Votre application gère maintenant l'onboarding des utilisateurs, la gestion des essais, les notifications par email, la synchronisation CRM et les rapports de churn — le tout avec une exécution durable, des retentatives automatiques et zéro gestion d'infrastructure.
Les points clés à retenir :
- Les fonctions durables survivent aux pannes et aux retentatives sans réexécuter les étapes terminées
- Le fan-out piloté par événements permet à plusieurs fonctions de réagir au même événement indépendamment
step.sleep()permet des workflows de longue durée (jours ou semaines) sans consommer de ressources de calculstep.waitForEvent()remplace le polling et les callbacks webhook par une coordination d'événements déclarative- La concurrence et le throttling protègent les API externes contre la surcharge
- Le serveur de développement vous donne une observabilité complète pendant le développement local
Inngest transforme la logique backend complexe et multi-étapes en fonctions TypeScript lisibles. Au lieu d'assembler des files de messages, des cron jobs et des machines à états, vous écrivez le workflow comme une seule fonction et laissez Inngest gérer le reste.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire des tâches de fond en production avec Trigger.dev v3 et Next.js
Apprenez à construire des tâches de fond fiables, des tâches planifiées et des workflows multi-étapes avec Trigger.dev v3 et Next.js. Ce tutoriel couvre la création de tâches, la gestion des erreurs, les retries, les cron jobs et le déploiement en production.

Upstash Redis et Next.js : Rate Limiting, Caching et Files de Messages
Apprenez à intégrer Upstash Redis dans une application Next.js pour implémenter le rate limiting, le caching côté serveur et les files de messages. Ce tutoriel couvre la configuration, les patterns de production et le déploiement serverless.

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.