Notifications

Référence technique du domaine Notifications — in-app, email transactionnel, SMS, templates, préférences utilisateur et queues persistantes.

Notifications

Périmètre : notifications in-app, email transactionnel, SMS transactionnel, templates configurables par organisation, préférences utilisateur, queues persistantes avec retry exponentiel, webhooks providers (Brevo, OVH SMS…).

Les écrans utilisateur vivent sous /account/notifications et /account/preferences. Les routes API se trouvent sous /api/account/notifications/*, /api/account/notification-preferences et /api/account/notifications-pause. Source de cette page : docs/15-domain-notifications.md.
Cible — non encore implémenté. Les templates configurables par organisation (EmailTemplate / SmsTemplate), l'écran admin /workspace/admin/settings/notifications et la route /api/workspace/admin/notifications/templates n'existent pas. Il n'y a pas non plus de tables EmailQueue / SmsQueue dédiées : l'envoi passe par la table générique Job (poller processDueJobs).
Garde-fou production — un email n'est jamais envoyé sans EMAIL_PROVIDER configuré. La valeur par défaut est console : les emails sont simplement loggés en stdout, ce qui est sûr en développement et évite tout envoi accidentel.

Vocabulaire & entités

TermeEntité / CodeDéfinitionStatut
NotificationNotificationMessage in-app (recipientUserId, kind, title, body, targetUrl, payload).implémenté
CanalNotificationChannelIN_APP, EMAIL, SMS.implémenté
TypeNotificationKind (enum)Déclencheur typé (MENTION, INVOICE_ISSUED, LEASE_AFFAIR_STATUS…).implémenté
PréférenceNotificationPreference1 ligne par (userId, kind, channel), booléen enabled.implémenté
File d'envoiJob (générique)Jobs notification:send-email / notification:send-sms exécutés par le poller.implémenté
TemplateEmailTemplate / SmsTemplateModèle de contenu paramétrable par organisation.cible (modèle absent)
Catalogueshared/notifications/catalog.tsSource de vérité des types et audiences.cible (fichier absent)

Le modèle complet est dans docs/04-data-model.md §12. Le type de notification est un enum Prisma NotificationKind, pas une chaîne libre.

Types de notification (NotificationKind)

Le typage réel est l'enum Prisma NotificationKind (pas un catalogue de codes hiérarchiques). Valeurs actuelles :
MENTION                COMMENT_REPLY
INVOICE_ISSUED         INVOICE_PAID
OT_ASSIGNED            LEASE_AFFAIR_STATUS
GENERIC
CRM_MISSED_CALL        CRM_ACTION_OVERDUE      CRM_ACTION_ASSIGNED
CRM_PROSPECT_PROMOTED
RATING_REQUESTED       RATING_VALIDATED        RATING_EXPIRING       RATING_NO_GO_ON_DEAL
Cible — non encore implémenté. La taxonomie hiérarchique en codes (lease.contract.*, sales.invoice.*, ai.workflow.*…) et le catalogue centralisé shared/notifications/catalog.ts (templates + audiences par défaut) sont une cible.

Écrans & préférences

ÉcranRouteContenuStatut
Centre de notifications/account/notificationsListe paginée, marquer lu / tout lu. Tap → marque lu + redirige vers targetUrl.implémenté
Icône topbar(topbar bell icon)Badge compteur non lus.implémenté
Préférences/account/preferencesChoix canaux in-app / email / SMS par type ; mise en pause (until).implémenté
Templates admin/workspace/admin/settings/notificationsÉdition des templates par organisation.cible (non implémenté)

Modèle de données (points clés)

  • Notification : créée en in-app (createNotification) puis les jobs d'envoi email/SMS sont enqueués selon les préférences. Champs de tracking : emailMessageId, smsMessageId, emailStatus. Index (recipientUserId, readAt, createdAt).
  • NotificationPreference : clé unique (userId, kind, channel), booléen enabled.
  • L'envoi différé passe par la table générique Job (pas de table EmailQueue / SmsQueue) : jobs notification:send-email et notification:send-sms, retry avec backoff exponentiel géré par jobs.ts.

API

Les routes /api/account/* n'exigent qu'une session authentifiée — chaque utilisateur ne voit et ne modifie que ses propres notifications et préférences.

Routes implémentées

MéthodeRoutePermission
GET/api/account/notificationsauthentifié
POST/api/account/notifications/mark-readauthentifié
POST/api/account/notifications/mark-all-readauthentifié
GET/api/account/notifications/unread-countauthentifié
GET/api/account/notification-preferencesauthentifié
PATCH/api/account/notification-preferencesauthentifié
PATCH/api/account/notifications-pauseauthentifié
POST/api/webhooks/brevo/tracksecret (fail-closed)

Routes cible (non implémentées)

Cible — non encore implémenté. Pas de route /api/notifications ni /api/account/preferences/notifications (les vraies sont sous /api/account/notifications/* et /api/account/notification-preferences). Pas non plus de route templates ni de webhook SMS dédié.
MéthodeRoute ciblePermission cible
GET / POST/api/workspace/admin/notifications/templatesadmin.settings:read / :update
POST/api/webhooks/smsHMAC
PATCH/api/account/notification-preferencesAuth

Met à jour une préférence de notification de l'utilisateur courant (un couple type × canal à la fois).

Corps (JSON)

kind
NotificationKind required
Type de notification (valeur de l'enum NotificationKind, ex. INVOICE_ISSUED).
channel
NotificationChannel required
Canal ciblé : IN_APP, EMAIL ou SMS.
enabled
boolean required
Activer ou désactiver ce couple.

Requête

curl -s -X PATCH "$API/api/account/notification-preferences" \
  -H "Content-Type: application/json" -H "Cookie: $SESSION" \
  -d '{"kind":"INVOICE_ISSUED","channel":"SMS","enabled":false}'

Réponse

{ "kind": "INVOICE_ISSUED", "channel": "SMS", "enabled": false }
La mise en pause des notifications se fait via PATCH /api/account/notifications-pause avec un corps { "until": "<ISO8601>" | null } (null lève la pause). Le marquage lu prend un corps { "notificationIds": ["..."] } sur POST /api/account/notifications/mark-read.

Workflows

Émission d'une notification

Le service notification.service.ts expose createNotification(input), appelé par les autres services lors de mutations sensibles.

await createNotification({
  organizationId,
  recipientUserId: 'usr_...',
  kind: 'LEASE_AFFAIR_STATUS',
  title: 'Affaire mise à jour',
  body: 'Le contrat LLD-2026-000123 a changé de statut.',
  targetUrl: `/workspace/lease-affairs/${affairId}`,
  payload: { affairId },
})

Flux interne :

  1. Crée la Notification in-app (canal incompressible).
  2. dispatchChannelJobs : charge les NotificationPreference (userId, kind) et, pour chaque canal enabled :
    • EMAILenqueueJob('notification:send-email', { notificationId }).
    • SMSenqueueJob('notification:send-sms', { notificationId }).

Poller de jobs

Il n'y a pas de cron dédié aux emails : tous les jobs (dont notification:send-email / notification:send-sms) passent par la table générique Job, traitée par le poller toutes les 10 s (processDueJobs, jobs.ts).

Pour chaque job dû :
  1. Exécute le handler (sendNotificationEmail / sendNotificationSms)
  2. Succès → met à jour Notification.emailMessageId / emailStatus = 'sent'
  3. Échec + attempt < maxAttempts → reschedule avec backoff exponentiel
  4. Échec + attempt >= maxAttempts → FAILED + alertJobFailure

Providers email (EMAIL_PROVIDER)

console (défaut)

Valeur par défaut de EMAIL_PROVIDER. Logs en stdout uniquement — aucun envoi réel. Garde-fou en dev pour éviter tout trafic accidentel.

smtp

EMAIL_PROVIDER=smtp — MailHog en dev, Scaleway TEM en prod (SMTP).

brevo

EMAIL_PROVIDER=brevo — Brevo HTTP API (BREVO_API_KEY). Tracking via POST /api/webhooks/brevo/track.

Providers SMS

SMS via server/lib/sms.ts selon la configuration d'environnement.

Tracking livraison (webhook entrant)

  • POST /api/webhooks/brevo/track : route publique, vérifiée par un token simple en en-tête x-brevo-token (comparé à BREVO_WEBHOOK_SECRET, fail-closed si non configuré). Événements delivered / opened (clicked mappé sur opened) / bounced → mise à jour de Notification.emailStatus.
Cible — non encore implémenté. Le webhook SMS entrant (/api/webhooks/sms) n'existe pas. La vérification du webhook Brevo est un token partagé, pas une signature HMAC.

Règles métier

  • IN_APP toujours créée : la Notification in-app est le canal incompressible ; les canaux EMAIL/SMS dépendent des NotificationPreference.
  • Pause utilisateur : un utilisateur peut suspendre ses notifications jusqu'à une date (PATCH /api/account/notifications-pause).
  • RGPD : opt-out clair en footer pour les emails non strictement transactionnels.
Cible — non encore implémenté. Les règles suivantes décrivent la cible et n'ont pas d'implémentation à ce stade : désactivation automatique du canal EMAIL sur hard bounce / plainte spam, throttling SMS (1 / 5 min / type), templates système immuables surchargeables par organisation et audit log de modification de template, batch Brevo 1 000 destinataires.