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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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.created avant 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-saas

Installez Inngest :

npm install inngest

C'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 dev

Dans un terminal séparé, lancez votre application Next.js :

npm run dev

Le 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é)

  1. Créez un compte sur le site Inngest
  2. Obtenez votre clé de signature et clé d'événement depuis le tableau de bord
  3. Ajoutez les variables d'environnement :
# .env.production
INNGEST_SIGNING_KEY=signkey-prod-xxxxx
INNGEST_EVENT_KEY=eventkey-xxxxx
  1. Déployez votre application Next.js sur Vercel, Railway ou toute autre plateforme
  2. 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:latest

Dé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éInngestFile traditionnelle (BullMQ)Cron Jobs
Étapes durablesOuiManuelNon
Piloté par événementsOuiRoutage manuelNon
Sommeil de plusieurs joursOui (sans coût de calcul)Nécessite un worker persistantHacks de timer
Retentative avec backoffIntégréConfiguration manuelleManuel
Fan-outAutomatiqueRoutage manuelN/A
Développement localServeur de dev avec UIRedis requiscrontab
Sécurité de typageTypeScript completPartielAucune
ObservabilitéTableau de bord intégréOutils externesFichiers 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 calcul
  • step.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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Créer un interpréteur de code personnalisé pour les agents LLM.

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 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.

30 min read·