Système d'emails
Templates DB-driven, fallbacks, éditeur admin et liste exhaustive des templates.
Les emails sont DB-driven : un template par défaut est défini dans le code, mais l'admin peut l'override en base. Édition via une UI dédiée, sans redéploiement.
Architecture
lib/email-templates.ts // DEFAULT_TEMPLATES (fallback hard-codé)
lib/email.ts // sendTemplated() + senders publics
prisma/schema.prisma // model EmailTemplate
app/api/admin/emails // CRUD admin
app/[locale]/(dashboard)/emails/ // éditeur UIsendTemplated(key, locale, vars, to) :
- Cherche dans
EmailTemplate(key + locale) — utilise si trouvé - Sinon fallback sur
DEFAULT_TEMPLATES[key][locale] - Interpole les variables (
{name},{code}, etc.) - Envoie via Resend
Liste des templates
| Key | Variables clés | Quand |
|---|---|---|
welcome | {name}, {appUrl} | Inscription confirmée |
magic-link | {url} | Login par lien |
password-reset | {url} | Demande de reset |
email-verification | {code} | Verif email (code 6 chiffres) |
two-factor-code | {code} | Login 2FA |
login-alert | {device}, {os}, {browser}, {location}, {ip}, {when} | Nouveau device détecté |
subscription-trial-ending | {planName}, {when} | 3 jours avant fin de trial |
subscription-renewing | {planName}, {priceLabel}, {when} | 7 jours avant renouvellement |
subscription-canceled | {planName}, {when} | Cancel at period end |
subscription-ended | {planName} | Fin effective |
subscription-coupon-ending | {couponDescription}, {when} | Coupon expire bientôt |
payment-failed | {planName}, {priceLabel} | Échec de paiement |
payment-succeeded | {planName}, {priceLabel} | Paiement OK |
Variables globales toujours dispo : {appName}, {appUrl}, {name}.
Senders publics
Wrappers typés dans lib/email.ts :
sendWelcomeEmail(to, name, locale)
sendMagicLinkEmail(to, url, locale)
sendPasswordResetEmail(to, url, locale)
sendVerificationCodeEmail(to, code, locale)
sendTwoFactorCodeEmail(to, code, locale)
sendLoginAlertEmail({ to, userName, browser, os, device, ip, country, city, locale })
sendTrialEndingEmail(...)
sendRenewingEmail(...)
// etc.Éditeur admin
Route : /admin/emails. Pour chaque template :
- Édition FR + EN côte à côte
- VAR_CHIPS : boutons qui insèrent
{name},{code}, etc. au curseur - Preview live avec valeurs d'exemple
- Bouton Reset to default par template/langue (delete row DB → fallback)
API : GET /api/admin/emails, PATCH /api/admin/emails/[key].
Ajouter un nouveau template
- Ajoute la clé dans le type
EmailTemplateKey(lib/email-templates.ts). - Ajoute les entrées FR + EN dans
DEFAULT_TEMPLATES. - Crée un sender wrapper dans
lib/email.ts:export async function sendMyNewEmail(to: string, vars: MyVars, locale: SupportedLocale) { return sendTemplated("my-new-key", locale, vars, to); } - L'éditeur admin le détecte automatiquement (il itère sur les clés).
EMAIL_FROM
EMAIL_FROM=noreply@toi.comDoit être un domaine vérifié dans ton compte Resend. En dev, onboarding@resend.dev
fonctionne mais n'envoie qu'à ton email de compte Resend.
Allez plus loin
- Webhooks Stripe — quand chaque email subscription part
- i18n — locales supportées
- Authentification