Aller au contenu

Notifications · Guide pratique

Configuration des notifications et des courriels

Construisez une boîte de réception durable, un moteur de livraison et un chemin direct pour les courriels d'authentification avec le package de notifications KrakStack.

@krak-stack/notifications est désormais le backend par défaut des nouveaux systèmes de notifications durables. Il réunit des migrations PostgreSQL et des opérations CRUD gérées par le package, une file PersistedQueue d'Effect, les workers de livraison et de rappels, un planificateur du travail échu ainsi que des transports SES et SMTP facultatifs.

Les anciens éléments du registre shadcn restent disponibles pour assurer la compatibilité. Ils copient un code source modifiable dans l'application, mais ne constituent plus le moteur durable recommandé.

Matrice des responsabilités

SujetResponsabilité de @krak-stack/notificationsResponsabilité de l'application
StockageMigrations du package; CRUD de la boîte de réception; CRUD des préférences et des blocages; enregistrements de livraison, tentative, référence, publication et rappelProvisionnement de la base, sauvegardes, politique de rétention et couche PgClient partagée
Moteur de livraisonTâches PersistedQueue d'Effect, nouvelles tentatives, réconciliation, planificateur et workers de livraison et de rappelsTopologie des processus, secrets des transports, supervision et alertes d'exploitation
Sécurité et périmètreEntrées de service tenant compte du périmètre et schémas EffectAuthentification, autorisation, périmètre fiable du destinataire, de l'organisme et de l'espace de travail, puis isolation des tenants
Politique produitComportement des préférences selon le caractère transactionnel ou facultatif et exécution des rappels ponctuelsChoix des destinataires, politique événementielle, récurrence, composition et cadence des récapitulatifs, obligations légales
ContenuSnapshots rendus persistés et validation du payload courriel indépendante du fournisseurTemplates, rendu, localisation, liens et identité visuelle
Frontière applicativeServices Effect côté serveurAPI HTTP, handlers, atoms, routes, état de l'interface et invalidation du cache
FournisseursContrat de transport et adaptateurs SESv2 et SMTPChoix du fournisseur, comptes, secrets, identités d'expéditeur et politique de délivrabilité

Ne considérez jamais un identifiant de destinataire, d'organisme ou d'espace de travail reçu du navigateur comme un périmètre fiable. Dérivez ce périmètre du contexte serveur authentifié avant d'appeler le package.

Installer le package durable

Installez le package avec le pilote PostgreSQL d'Effect :

shell

Ajoutez uniquement le fournisseur de courriel utilisé par l'application :

shell
# Amazon SESv2
bun add @aws-sdk/client-sesv2

# Ou SMTP avec Nodemailer
bun add nodemailer

Le service principal est exporté par @krak-stack/notifications, le moteur par /runtime, le contrat de transport par /transport, le schéma courriel sans fournisseur par /transport/email et les adaptateurs par /transport/email-ses et /transport/email-smtp.

Composer un seul client PostgreSQL partagé

Fournissez la même couche PgClient, donc le même pool, à NotificationService, à NotificationRuntime et à la file. Lors d'une publication immédiate, l'enregistrement de livraison, la tentative et la tâche de file sont alloués dans une transaction SQL contextuelle unique. Une deuxième couche client romprait cette atomicité.

La composition SES ci-dessous exécute les migrations du package, fournit le registre des transports et démarre le worker de livraison, le worker de rappels et le planificateur intégré :

ts
import { PgClient } from "@effect/sql-pg";
import { Config, Effect, Layer } from "effect";
import { NotificationService } from "@krak-stack/notifications";
import {
  NotificationRuntime,
  ReminderHandlerRegistry,
  notificationWorkersLayer,
} from "@krak-stack/notifications/runtime";
import { sesEmailTransportRegistryLayer } from "@krak-stack/notifications/transport/email-ses";

const pgClientLayer = Layer.unwrap(
  Effect.gen(function* () {
    const url = yield* Config.Redacted("DATABASE_URL");
    return PgClient.layer({ url });
  }),
);

// Cette couche d'adaptateur fournit NotificationTransportRegistry pour "email".
const transportRegistryLayer = sesEmailTransportRegistryLayer;

const runtimeLayer = NotificationRuntime.makeLayer({
  schedulerIntervalMillis: 10_000,
}).pipe(
  Layer.provide(transportRegistryLayer),
  Layer.provide(ReminderHandlerRegistry.emptyLayer),
);

export const notificationsLayer = notificationWorkersLayer.pipe(
  Layer.provideMerge(runtimeLayer),
  // Acquérir le service migré avant le démarrage de toute boucle de worker.
  Layer.provideMerge(NotificationService.layer),
  Layer.provide(pgClientLayer),
);

Conservez notificationsLayer active dans un runtime serveur ou worker avec une portée durable. Layer.provideMerge(NotificationService.layer) termine les migrations du package avant d'acquérir la couche des workers. notificationWorkersLayer lance ensuite les trois boucles intégrées : livraison, rappels et planification. Si vous utilisez les rappels ponctuels, remplacez emptyLayer par ReminderHandlerRegistry.layer([...]) pour enregistrer les handlers de l'application.

Un fournisseur personnalisé implémente NotificationTransport. Le registre les regroupe par canal :

ts
import { NotificationTransportRegistry } from "@krak-stack/notifications/transport";

const transportRegistryLayer = NotificationTransportRegistry.layer([
  emailTransport,
  smsTransport,
]);

Un seul transport peut être enregistré pour un canal. N'enregistrez pas SES et SMTP ensemble sous email.

Publier une notification durable

L'application choisit le destinataire, résout le périmètre fiable et la langue, rend le contenu, puis transmet des snapshots immuables à publish :

ts
import { Effect } from "effect";
import { NotificationService } from "@krak-stack/notifications";

const publishApplicationApproved = Effect.gen(function* () {
  const notifications = yield* NotificationService;

  return yield* notifications.publish({
    idempotencyKey: `application:${applicationId}:approved:v1`,
    scope: {
      recipientUserId: user.id,
      organizationId: organization.id,
      workspaceId: workspace.id,
    },
    eventKey: "application.approved",
    eventVersion: 1,
    inbox: {
      locale: user.locale,
      title: rendered.inboxTitle,
      description: rendered.inboxDescription,
      href: `/applications/${applicationId}`,
      metadata: { applicationId },
    },
    deliveries: [
      {
        key: "email",
        channel: "email",
        purpose: "notification",
        template: "application-approved@1",
        recipientAddress: user.email,
        recipientName: user.name,
        payloadVersion: 1,
        payload: {
          subject: rendered.emailSubject,
          text: rendered.emailText,
          html: rendered.emailHtml,
        },
        references: [{ namespace: "application", value: applicationId }],
      },
    ],
  });
});

Une ligne de livraison représente un seul destinataire. L'adaptateur courriel déduit la destination de recipientAddress et recipientName; EmailPayloadV1 ne contient volontairement ni to, ni cc, ni bcc. Créez une livraison de publication par destinataire.

Avec purpose: "notification", les préférences et les blocages actifs d'adresse sont appliqués. Avec purpose: "transactional", les préférences sont ignorées, mais les blocages d'adresse restent actifs. Il appartient à l'application de déterminer quels événements sont transactionnels sur le plan légal ou opérationnel.

Configurer SES ou SMTP

L'adaptateur SES lit les variables serveur suivantes :

env
SES_REGION=ca-central-1
SES_ACCESS_KEY_ID=...
SES_SECRET_ACCESS_KEY=...
NOTIFICATION_EMAIL_FROM=[email protected]

Utilisez sesEmailTransportRegistryLayer depuis @krak-stack/notifications/transport/email-ses. Cette version accepte des clés d'accès SES explicites; la chaîne d'identifiants AWS par défaut et les jetons de session ne sont pas encore pris en charge.

Pour SMTP, remplacez la couche de registre dans l'exemple de composition :

ts
import { smtpEmailTransportRegistryLayer } from "@krak-stack/notifications/transport/email-smtp";

const transportRegistryLayer = smtpEmailTransportRegistryLayer;

L'adaptateur SMTP lit les variables ci-dessous, exige TLS et vérifie la connexion au démarrage de sa couche :

env
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=...
SMTP_PASSWORD=...
EMAIL_SENDER_ADDRESS=[email protected]

Les deux adaptateurs acceptent un subject, un text, un html facultatif, un from facultatif et un replyTo facultatif déjà rendus dans la version 1 du payload courriel. Au moins un champ parmi text et html est requis.

Livraisons planifiées, nouvelles tentatives et rappels

Les livraisons futures et les nouvelles tentatives restent dans le registre du package jusqu'à l'exécution de NotificationRuntime.enqueueDue. Une livraison planifiée exige donc le planificateur intégré, soit au moyen de notificationWorkersLayer comme ci-dessus, soit en exécutant vous-même NotificationRuntime.runScheduler. Démarrer uniquement le worker de livraison ne place pas le travail futur dans la file.

Les effets externes des fournisseurs et des handlers de rappel sont exécutés au moins une fois. Un processus peut s'arrêter après l'acceptation du message par le fournisseur, mais avant la validation de la transaction de succès. La même génération peut alors repartir. Lorsque le fournisseur le permet, utilisez l'identifiant stable de distribution ou de tâche comme clé d'idempotence.

Un rappel ponctuel persiste une clé et une version de handler ainsi qu'un payload, puis passe par la file de rappels. Le package gère la planification, les nouvelles tentatives, le changement de date, l'annulation et l'exécution de cette invocation unique. L'application définit les règles de récurrence, crée l'occurrence suivante et choisit les événements d'un récapitulatif.

Propriété de la base et des migrations

NotificationService.layer exécute les migrations du domaine avant de rendre le service disponible. Le package possède les tables suivantes :

  • notifications
  • notification_settings
  • notification_deliveries
  • notification_delivery_attempts
  • notification_delivery_references
  • notification_suppressions
  • notification_reminders
  • notification_publications
  • krakstack_notification_migrations

La PersistedQueue d'Effect possède krakstack_notification_jobs et krakstack_notification_jobs_migrations.

Au démarrage, la file exécute actuellement du DDL Effect lorsque la couche de service ou de runtime construit le store persistant. NotificationService.layerWithoutMigrations désactive le migrateur du domaine du package, mais pas ce DDL de démarrage de la file. Si le rôle de déploiement ne peut pas exécuter de DDL, provisionnez au préalable les objets du domaine et de la file, puis vérifiez ce comportement avec la version d'Effect installée.

Excluez toutes les tables de domaine, de file et de migration appartenant au package des entrées et de la génération de migrations Drizzle de l'application. Si l'application définit des mappings en lecture seule pour ces tables, configurez Drizzle Kit afin qu'il ne les gère pas. Deux systèmes de migration ne doivent jamais modifier les mêmes tables.

Adopter les anciennes tables de notifications

Les migrations du package peuvent reprendre les trois tables auparavant installées par notification-persistence sans supprimer leurs données. Procédez dans cet ordre :

  1. Sauvegardez et conservez notifications, notification_settings et notification_deliveries; ne les supprimez pas et ne les recréez pas.
  2. Arrêtez tous les relais et workers de notifications appartenant à l'application avant de démarrer le nouveau runtime. Les anciens et nouveaux workers ne doivent jamais réclamer les mêmes livraisons en parallèle.
  3. Retirez les tables du package des entrées de migration Drizzle de l'application. Conservez les lignes PostgreSQL existantes, mais confiez désormais l'historique du schéma au package.
  4. Démarrez NotificationService.layer avec le PgClient partagé afin que ses migrations additives adoptent le schéma avant le lancement des workers du package.
  5. Déplacez les corrélations propres à l'application vers notification_delivery_references avec des espaces de noms stables. La migration importe les colonnes compatibles de notification_delivery_correlations sous application, opportunity et organization; migrez explicitement toute autre forme de corrélation.
  6. Vérifiez les nombres de lignes et les statuts, puis démarrez notificationWorkersLayer et surveillez les premiers passages du planificateur et des workers.

Pendant l'adoption, les anciennes livraisons processing qui n'ont pas de tâche Effect courante passent à retrying lorsque leur budget de tentatives le permet, ou à failed lorsqu'il est épuisé. Leurs anciennes réclamations sont effacées. Les lignes désactivées d'une table email_preferences compatible deviennent des blocages courriel pour purpose: "notification"; aucune valeur de remplacement n'est nécessaire pour les lignes actives, car les notifications sont autorisées par défaut.

Utiliser le mode direct pour l'authentification

Les courriels de code à usage unique, de vérification et de réinitialisation de mot de passe de type KrakAuth ont souvent besoin d'un simple appel fournisseur dans la requête, sans boîte de réception ni registre durable. Le mode direct réutilise le même registre de transports, sans PostgreSQL ni file :

ts
import { Effect, Layer } from "effect";
import {
  DirectNotificationDispatcher,
  dispatchDirect,
} from "@krak-stack/notifications/direct";
import { sesEmailTransportRegistryLayer } from "@krak-stack/notifications/transport/email-ses";

const directEmailLayer = DirectNotificationDispatcher.layer.pipe(
  Layer.provide(sesEmailTransportRegistryLayer),
);

const sendPasswordReset = dispatchDirect({
  scope: {
    recipientUserId: user.id,
    organizationId: null,
    workspaceId: null,
  },
  eventKey: "auth.password-reset",
  eventVersion: 1,
  channel: "email",
  template: "password-reset@1",
  recipientAddress: user.email,
  recipientName: user.name,
  payloadVersion: 1,
  payload: {
    subject: rendered.subject,
    text: rendered.text,
    html: rendered.html,
  },
}).pipe(Effect.provide(directEmailLayer));

Le mode direct ne fournit ni persistance, ni planification de nouvelles tentatives, ni entrée de boîte de réception, ni historique de livraison, ni consultation des préférences ou des blocages. Le flux d'authentification appelant doit décider des conséquences d'un échec du fournisseur et ne doit pas annoncer de garantie de livraison durable.

Garder les API et l'interface dans un périmètre sûr

Le package ne fournit volontairement ni groupe HTTP, ni handler, ni atom, ni route, ni interface. Encapsulez les méthodes de NotificationService dans des API applicatives qui dérivent l'acteur et le périmètre du contexte d'authentification fiable. Ne retournez que les enregistrements autorisés, puis invalidez la requête client correspondante après chaque mutation.

Pour installer une interface sous forme de code source, utilisez le menu indépendant du backend :

shell
bunx shadcn@latest add @krak-stack/notification-menu

NotificationMenu reçoit des enregistrements déjà autorisés et remonte leurs identifiants par des callbacks contrôlés. L'application adapte les enregistrements de boîte de réception du package, gère la navigation et fournit le contenu localisé ainsi que l'identité visuelle du produit.

Anciennes options avec code source copié

  • service-notification est un distributeur direct de compatibilité pour les applications qui utilisent déjà des messages indexés par canal. Son hook de store applicatif facultatif n'ajoute ni migrations du package, ni file persistante, ni réconciliation, ni workers.
  • notification-channel-email-ses est le canal SES correspondant à cet ancien distributeur. Les utilisateurs du nouveau package importent plutôt @krak-stack/notifications/transport/email-ses.
  • notification-persistence installe les anciennes définitions Drizzle à trois tables et leurs codecs comme code source modifiable. Une nouvelle application durable ne doit pas l'installer; elle doit utiliser @krak-stack/notifications et ses migrations.
  • notification-menu reste un composant de présentation pris en charge et indépendant du backend.

Ne composez pas l'ancien distributeur ou un worker appartenant à l'application avec le nouveau runtime pour un même événement. Choisissez un seul propriétaire de la livraison pendant la migration afin d'éviter les doubles envois.

Liste de vérification pour la production

  • Utilisez un seul pool PgClient pour le service, le runtime et les files.
  • Exécutez les migrations du package avant ses workers et excluez ses tables des migrations Drizzle.
  • Exécutez le planificateur intégré dès que les livraisons planifiées ou les nouvelles tentatives sont actives.
  • Traitez les effets des fournisseurs et des rappels comme des opérations exécutées au moins une fois.
  • Autorisez chaque opération sur la boîte de réception, les préférences, les blocages, les livraisons et les rappels à la frontière serveur.
  • Rendez et localisez les snapshots avant publication; conservez les versions des templates dans le code de l'application.
  • Surveillez les échecs terminaux, la réconciliation des tâches obsolètes, la profondeur des files, les limites fournisseur et la réputation de l'expéditeur.
  • Testez le contenu anglais et français, l'adoption des migrations et les arrêts autour de l'acceptation du fournisseur.
  • Ne journalisez jamais les secrets ni le contenu sensible des messages.

Références connexes