Documentation

Email logs

Tracking des emails envoyés via Resend — statuts livré/bounce/complained/opened, page admin, webhook.

Tous les emails envoyés via lib/email.ts ou les broadcasts sont loggés en DB. Les statuts (delivered, bounced, complained, opened) sont mis à jour par le webhook Resend.

Modèle de données

model EmailLog {
  id           String    @id @default(cuid())
  userId       String?
  email        String
  subject      String
  templateKey  String?               // welcome / magic-link / broadcast / null = ad-hoc
  providerId   String?   @unique     // Resend message id (clé du webhook)
  status       String    @default("queued")  // queued | sent | delivered | bounced | complained | failed
  deliveredAt  DateTime?
  bouncedAt    DateTime?
  complainedAt DateTime?
  openedAt     DateTime?
  errorMsg     String?
  createdAt    DateTime  @default(now())
}

Hook automatique

lib/email.ts → sendTemplated() capture systématiquement le résultat Resend et appelle logEmail :

const result = await resend.emails.send({ from, to, subject, html });
await logEmail({
  to: opts.to,
  subject,
  templateKey: opts.templateKey,
  providerId: result.data?.id ?? null,
  errorMsg: result.error?.message ?? null,
});

Idem côté lib/broadcasts.ts (templateKey = "broadcast"). Si l'envoi throw (réseau), logEmail est aussi appelé avec errorMsg pour ne pas perdre la trace.

Redaction auto des codes sensibles

logEmail masque les chiffres de 4+ dans le subject pour les templates email-verification, two-factor-code, magic-link, password-reset :

"Ton code : 123456"  →  "Ton code : ******"

Pour qu'un admin lisant les logs ne puisse pas reusiner un code OTP.

Webhook Resend

Endpoint : POST /api/webhooks/resend. Configure dans Resend dashboard → Webhooks :

  • URL : ${SITE_URL}/api/webhooks/resend
  • Events : email.delivered, email.bounced, email.complained, email.opened
  • Secret : copie dans .envRESEND_WEBHOOK_SECRET=whsec_...

Resend signe via Svix (headers svix-id, svix-timestamp, svix-signature). Le code vérifie HMAC-SHA256 sur ${svix-id}.${svix-timestamp}.${rawBody}, refuse les replays > 5 min, puis met à jour la row matching providerId.

Page admin

/admin/emails/logs — liste paginée avec :

  • Recherche email / sujet
  • Filtres status (all / delivered / sent / bounced / complained / failed)
  • Vue table (desktop) + cards (mobile) via <DataList>
  • Détail par row : statut, template, erreur si échec

Onglet Emails sur la fiche user (/admin/users → click) → 50 derniers envois pour cet user (utile pour le support : "pourquoi je ne reçois pas mes mails ?").

Allez plus loin

On this page