Messaging, notifications, emails et push
Cette page décrit la chaîne messaging/notification vérifiée dans les modules Convex.
Sources principales :
convex/messaging/events.ts
convex/messaging/bus.ts
convex/messaging/notifications.ts
convex/email/outbox.ts
convex/email/dispatch.ts
convex/email/resend.ts
convex/social/recipient_groups.ts
convex/messaging/push.ts
convex/social/notifications.ts
web/src/hooks/usePushNotifications.ts
web/src/components/pwa/PwaServiceWorkerBootstrap.tsx
web/public/sw.js
Modèle d’événements
Le format d’entrée est MessagingEvent (validator dans events.ts) et les événements sont transformés en plan d’envoi (in-app + emails) par buildPlan dans bus.ts.
Événements observés (non exhaustif des payloads, mais exhaustif des kind listés dans le validator) :
auth.password_reset_requested
auth.email_verification_requested
feedback.submission_succeeded
feedback.submission_failed
ea.api.health_alerted
ea.potential_draw_review_required
user.unbanned
user.banned
user.reactivated
user.deleted
person_sanction.activated
person_sanction.ended
competition_ban.activated
competition_ban.ended
admin.bet_bulk_job.updated
admin.bet_bulk_job.partial_failure_alerted
admin.user_premium.updated
bet.status_changed
ea.score.confirmed
ea.score.unavailable
ea.score.ambiguous
ea.score.error
transfer.invitation.*
transfer.join_request.*
transfer.premium_break.executed
transfer.player.left_source_club
totw.published
season_awards.published
match.scheduling.*
match.dispute.reported
match.dispute.resolved
match.report.reported
match.postponement.*
match.result.auto_validated
match.result.diverged
match.result.admin_validated
match.result.contested
league.calendar.deleted
article.published
Variantes exactes des familles abrégées :
transfer.invitation.created|expired|rejected|accepted|cancelled
transfer.join_request.created|expired|rejected|accepted|cancelled
match.scheduling.proposed|accepted|rejected|cancelled
match.postponement.proposed|accepted|counterProposed|rejected
Bus dispatch
dispatch (Mutation interne) :
- construit le plan via
buildPlan,
- applique
deliverInApp,
- résout les destinataires email,
- planifie l’action email
internal.email.dispatch.send via scheduler runAfter(0, ...).
Les événements competition_ban.activated et competition_ban.ended ciblent
explicitement l’utilisateur sanctionné en in-app. L’email passe par la résolution
users, donc une adresse vide ou invalide produit uniquement la notification
in-app.
dispatchAction (Action interne) :
- même logique de plan,
- écrit in-app via
recordInApp mutation,
- envoie emails via
internal.email.dispatch.send en action (pas de wrapper scheduler).
Le résumé de retour contient :
inApp.recipients|inserted|duplicates|skipped|notificationIds
email.requested|queued|failed
Fanout in-app et règles de sécurité
inAppTarget supportés : user, users, group (voir inAppTarget dans notifications.ts).
- Cibles de groupe via
resolveRecipientGroupMembers (social/recipient_groups.ts).
- Dedup :
- dédup par
(userId, dedupKey),
- pour
users, la key est suffixée par :<userId> pour éviter collision inter-destinataires.
- les bans compétition utilisent
competition_ban:{sanctionId}:activated et
competition_ban:{sanctionId}:ended.
match.scheduling.*, match.postponement.*, league.calendar.deleted ont des cibles explicites imposées (pas de groupe) via assertSafeInAppTarget.
assertTargetedInAppPlan exige pour ces événements :
- exactement une enveloppe in-app,
- cibles utilisateur explicites cohérentes avec les
recipientUserIds / managerUserIds,
- cap
MAX_MATCH_TARGETED_IN_APP_RECIPIENTS = 40.
Push
- push planifié par
schedulePushDeliveries en lots,
PUSH_DELIVERY_BATCH_SIZE = 200,
- chaque lot via
scheduler.runAfter(0, dispatchInAppNotifications).
Abonnement navigateur
usePushNotifications :
- vérifie la présence de
serviceWorker, PushManager et Notification,
- demande la permission navigateur lors de l’activation,
- exige
NEXT_PUBLIC_VAPID_PUBLIC_KEY,
- crée ou réutilise l’abonnement
PushManager,
- remplace l’abonnement si sa clé VAPID ne correspond plus,
- persiste
endpoint, p256dh, auth, installationId et userAgent via
upsertPushSubscription,
- réconcilie l’abonnement au chargement, au focus, au retour réseau et au retour
de visibilité,
- désabonne le navigateur et désactive la préférence côté Convex à la
désactivation.
Le contrôle est exposé dans NotificationBell.
Service worker et livraison
PwaServiceWorkerBootstrap enregistre /sw.js uniquement en production et
dans un contexte sécurisé (https, localhost ou 127.0.0.1). En
développement, il supprime les anciens workers et caches.
web/public/sw.js :
- gère les événements
push,
- affiche la notification avec icône/badge par défaut,
- refuse les URL externes dans le payload et retombe sur
/dashboard,
- gère le clic de notification vers une fenêtre existante ou une nouvelle
fenêtre,
- fournit aussi le cache PWA et la page
/offline.
Le backend charge web-push dynamiquement et exige
VAPID_SUBJECT, NEXT_PUBLIC_VAPID_PUBLIC_KEY et VAPID_PRIVATE_KEY. Si la
configuration ou le runtime manque, la livraison push est ignorée avec un
warning sans bloquer les notifications in-app.
Outbox / retry email
Queue
enqueueEmailBatch (email/outbox.ts) :
- déduplique par
dedupKey (index by_dedupKey),
- si existant + status !=
FAILED : skip,
- si existant +
FAILED : requeue (status PENDING, attemptCount reset),
- sinon insert
PENDING,
- déclenche
processDueEmailsAction avec limite par défaut DEFAULT_BATCH_SIZE = 20 (max MAX_BATCH_SIZE = 50).
Traitement batch
processDueEmailsAction :
claimDueEmails prend les due rows PENDING et passe en SENDING,
- envoie via
fetch("https://api.resend.com/emails"),
- en succès :
markEmailSent,
- en échec :
markEmailAttemptFailed,
- si taille du lot atteinte, auto replanifie le batch suivant.
Backoff/retry
- max tentatives:
MAX_RETRY_ATTEMPTS = 8,
- délai progressif
BASE_RETRY_DELAY_MS = 60_000 avec jitter + cap MAX_RETRY_DELAY_MS = 6h,
- status:
- réessai jusqu’au max :
PENDING + nextAttemptAt,
- au max atteint :
FAILED.
markEmailAttemptFailed planifie de nouveau processDueEmailsAction jusqu’à épuisement.
Envoi effectif
email/dispatch.ts expose send pour la couche unifiée.
sendEmailBatchCore (email/resend.ts) :
Promise.allSettled par destinataire,
- journalisation monitoring via
recordSendAttempts vers email_delivery_logs,
- retourne
{ requested, queued, failed }.
Legacy sendEmailBatch reste présent mais délègue au même core (sendEmailBatchCore) pour compatibilité admin.Last modified on July 16, 2026