10 jours sur l'invisible : construire un systeme d'emails qui tient la production
10 jours. 30 commits. Zéro nouvelle feature visible. Pendant que les utilisateurs de TAMSIV attendaient un nouveau bouton, une nouvelle couleur ou une nouvelle fonction vocale, j'ai passé chaque journée à réécrire un truc que personne ne verra jamais : la façon dont l'app parle aux gens par email. Et j'ai réalisé que l'invisible pèse peut-être pour 80% de ce qui fait qu'un produit reste installé.
Points clés à retenir
- Un système d'emails transactionnels complet : rappel de vérification, feedback loop J+7, préférences, désinscription propre, historique par user.
- Un webhook Resend qui écoute les 6 types d'events (delivered, opened, clicked, bounced, complained, delivery_delayed) pour agréger les métriques en temps réel.
- Un flow anti-usurpation RGPD : un utilisateur peut signaler un email de bienvenue qu'il n'a jamais demandé, on supprime l'inscription immédiatement.
- Un cron quotidien qui envoie un rappel unique aux comptes non vérifiés depuis 3 jours, i18n 6 langues.
- Deux hotfix mobile en parallèle (v1.07 + v1.08) : audit gesture-handler, refonte UI du feed, modales stables, 8 bugs corrigés.
Pourquoi construire un système d'emails entier quand Supabase Auth envoie déjà les mails de vérification
C'est une question légitime. Supabase, dans sa configuration par défaut, envoie déjà un email de vérification à chaque signup. Beaucoup de produits s'arrêtent là et n'ajoutent rien d'autre. Ça suffit pour valider un email, pas pour construire une relation avec l'utilisateur.
En production depuis le 4 avril, je me suis retrouvé avec des dizaines de comptes créés mais non vérifiés. Pas de relance. Pas de feedback à J+7 pour savoir si l'app leur était utile. Pas de moyen propre pour quelqu'un de dire "cet email n'était pas pour moi". Et surtout, aucune visibilité côté admin sur ce qui partait, arrivait, rebondissait ou était marqué comme spam.
Un système d'emailing transactionnel maison, c'est exactement ce qui sépare un produit "techniquement fonctionnel" d'un produit qui a l'air sérieux. Les grandes apps le cachent derrière leur polish. Les petites apps le négligent et perdent des utilisateurs sans comprendre pourquoi.
Le cron quotidien qui envoie un rappel, et UN SEUL
Le premier brick posé, c'est un cron quotidien qui tourne à heure fixe sur Vercel. Il interroge la base, sélectionne les comptes créés depuis exactement 3 jours et non encore vérifiés, et déclenche un rappel unique par utilisateur.
[PERSONAL EXPERIENCE] La règle "un seul rappel" est volontaire. J'ai reçu plus d'emails de relance abusive que je ne peux compter. Trois rappels en 48h, cinq en une semaine, dix sur un mois. C'est le meilleur moyen de faire désabonner les gens avant même qu'ils n'aient essayé le produit.
Le cron écrit dans une table dédiée pour savoir qui a reçu quoi et quand. Si un utilisateur est déjà relancé, il est skip. Si l'envoi Resend échoue, l'erreur est logguée mais le cron ne retente pas automatiquement : je préfère une visibilité sur les échecs à une saturation silencieuse.
Le feedback loop à J+7 : 4 boutons, 4 vérités
[UNIQUE INSIGHT] Sept jours après l'inscription, chaque nouvel utilisateur reçoit un email avec 4 boutons cliquables : j'adore, je déteste, j'ai une suggestion, j'ai trouvé un bug. Chaque bouton pointe vers une page dédiée qui enregistre la réponse et propose une zone de commentaire libre.
Pourquoi cette forme précise ? Parce qu'une note Google Play est filtrée par celui qui a le plus d'énergie à la laisser. Un email avec 4 boutons capture la vérité silencieuse. Celle des gens qui n'iront jamais sur le store écrire un commentaire, mais qui en un clic peuvent dire "ça ne m'a pas plu".
Techniquement, chaque bouton porte un token signé relatif à l'utilisateur et au type de retour. La route GET sur le site valide le token, enregistre la réponse avec le user_id et le type, et affiche la page correspondante avec un formulaire libre. Pas d'auth supplémentaire pour laisser un feedback : c'est volontaire. La friction tue les retours.
Le flow anti-usurpation : RGPD sans drame
Cas concret : quelqu'un crée un compte TAMSIV avec l'email alice@exemple.fr. Mais Alice n'a rien demandé. Elle reçoit un email de bienvenue pour un compte qu'elle n'a jamais créé. Qu'est-ce qu'elle fait ?
Sans ce flow, elle range l'email dans les spams ou signale l'envoyeur. Dans les deux cas, c'est une perte : perte pour moi côté réputation d'envoi, perte pour elle qui n'a aucun moyen propre de clore le sujet, perte pour la personne qui a fait la faute de frappe et qui ne recevra plus jamais de communication sur la bonne adresse.
Dans la nouvelle version, chaque email de bienvenue contient un lien "je n'ai pas créé ce compte". Clic, page dédiée, confirmation. L'inscription est immédiatement supprimée en base, un log est stocké pour audit, et un message de fin confirme à Alice que son adresse ne sera plus utilisée. RGPD sans drame. Une action, trois secondes, c'est fini.
[ORIGINAL DATA] La page est traduite dans les 6 langues de l'app (français, anglais, allemand, espagnol, italien, portugais). Les caractères accentués étaient cassés sur certaines langues à cause d'un problème d'encodage dans le fichier de traduction : fix dans le commit 12e61a7.
Le webhook Resend : savoir avant que l'utilisateur n'écrive
Tous les envois passent par Resend, un provider d'emails transactionnels focus developer experience. Resend propose des webhooks pour chaque événement du cycle de vie d'un email : email.sent, email.delivered, email.opened, email.clicked, email.bounced, email.complained, email.delivery_delayed.
Tous sont écoutés et stockés dans une table dédiée avec la référence à l'user, le type d'email, l'horodatage et le metadata associé. Ça permet ensuite d'agréger : combien d'emails sont arrivés aujourd'hui, combien ont été ouverts, lesquels ont généré un clic, combien ont rebondi.
Le dashboard admin affiche ces stats en temps réel avec des badges qui s'incrémentent à chaque envoi. Un bounce qui apparaît sur un user ? Je le vois immédiatement, je peux vérifier si c'est un problème temporaire ou un email mort, et ajuster. Un complain (signalement comme spam) ? Priorité haute, investigation immédiate.
L'historique d'envoi groupé par user, avec resend et dedup
L'admin voit tous les envois récents, mais groupés par utilisateur. Si Alice a reçu son email de vérification puis son rappel J+3 puis son feedback J+7, je vois une ligne "Alice" avec trois sous-lignes en expansion. Plus lisible qu'un flux chronologique brut.
Chaque envoi a un badge "source" (auto / manual). Les envois automatiques (cron, triggers) sont différenciés des envois manuels (bouton "resend" dans l'admin). Un bouton "renvoyer" existe sur chaque ligne pour les cas où un email est arrivé en spam, pour forcer un retry sur une autre alias, ou pour tester un changement de template.
Un système de dedup empêche de re-spammer : si j'ai envoyé un email de bienvenue il y a 2 heures et que je veux faire un envoi groupé "toutes les inscriptions du jour", Alice est exclue automatiquement parce qu'elle a déjà reçu. Un badge "éligible" s'affiche sur chaque user pour indiquer s'il peut recevoir l'envoi en cours ou non.
Deux hotfix mobile en parallèle : v1.07 et v1.08
Pendant que le backend emailing avançait sur Vercel, l'app mobile avait besoin d'air. Deux releases sont parties sur le Play Store en Alpha, puis production.
v1.07 (7d65fd0) : 8 bugs ciblés, plus une nouvelle vue compacte dans les dossiers. Quand tu as 15 sous-classeurs dans un projet, tu veux voir l'arborescence d'un coup d'œil sans scroller. Un toggle bascule entre vue détaillée (cartes complètes avec thumbnails et previews) et vue compacte (lignes denses avec juste le nom et le count). Zéro feature nouvelle en apparence, mais un changement d'usage pour les heavy users.
v1.08 (a494e7e) : audit complet gesture-handler. TAMSIV utilise react-native-gesture-handler partout, mais plusieurs écrans avaient encore des TouchableOpacity ou des FlatList importés depuis react-native directement. Résultat : des taps intermittents qui ne passaient pas, impossible à reproduire, signalés par des utilisateurs qui finissaient par désinstaller sans comprendre.
L'audit a touché 25+ fichiers. Chaque TouchableOpacity, chaque FlatList, chaque ScrollView a été basculé sur l'import de gesture-handler. Le bug des taps fantômes est corrigé. Profite-en pour refondre l'UI du feed et stabiliser toutes les modales : au passage, le polish fait un saut net.
Ce que j'en retire après 10 jours
Le truc qui m'a frappé, c'est que chaque ligne de code écrite pendant ces 10 jours va rester invisible tant qu'elle marche. Personne ne dit "waouh, ton email de vérification est arrivé pile au bon moment, une seule fois, avec la bonne langue". Personne ne remarque qu'un bounce a été détecté automatiquement et que l'admin a vu passer le signal avant même que l'user n'écrive.
L'invisible se remarque seulement quand il foire. Et à ce moment-là, l'utilisateur ne comprend pas ce qui a cassé : il comprend juste que l'app "ne marche pas bien". Il désinstalle. Il n'écrit pas. Il ne dit pas pourquoi.
Donc l'invisible, c'est peut-être 80% de ce qui fait un produit fini. Pas les boutons qu'on ajoute en feature releases, pas les couleurs qu'on refait, pas les animations. Juste le fait que quand quelque chose doit arriver chez l'utilisateur, ça arrive. Au bon moment. Une fois. Dans la bonne langue.
C'est pas sexy à poster sur LinkedIn. Mais c'est ce qui fait qu'un produit reste installé.
FAQ
Pourquoi ne pas utiliser un service tout-en-un comme Mailchimp ou ConvertKit ?
Parce que ce sont des outils de newsletter, pas de transactionnel. Ils sont conçus pour des envois groupés éditoriaux, pas pour des triggers individuels liés au cycle de vie de l'utilisateur. Resend est conçu pour les emails transactionnels (bienvenue, vérification, password reset, feedback loop). C'est le bon outil pour le bon job.
Le webhook Resend est-il fiable pour les events ?
Très fiable. Resend retente les webhooks en cas d'échec, signe les payloads pour l'authentification, et expose une console pour rejouer manuellement. Dans 10 jours d'utilisation, je n'ai pas eu un seul event perdu détecté.
Comment gérer les traductions des 6 langues pour les emails ?
Chaque template d'email est défini en français, puis traduit par un script automatique via OpenRouter (LLM). Les clés de traduction vivent dans les mêmes fichiers messages/*.json que le site, avec un namespace emails dédié. L'envoi côté serveur prend la langue préférée de l'utilisateur stockée en base, fallback en anglais si absente.
Le flow anti-usurpation est-il abusé ? Peut-on supprimer le compte de quelqu'un d'autre ?
Non. Le lien "je n'ai pas créé ce compte" est signé avec un token lié à l'adresse email cible. Seule la personne qui reçoit l'email peut cliquer dessus et supprimer l'inscription. Si quelqu'un d'autre tente d'appeler la route sans le bon token, elle renvoie une erreur 403 sans rien faire.
Est-ce que ce système s'applique à iOS quand l'app sortira ?
Oui, c'est backend. Les emails partent depuis Vercel et ne dépendent pas de la plateforme de l'app mobile. Le jour où TAMSIV sort sur iOS (12 personnes ont déjà cliqué sur "Télécharger pour iOS" sur le site), le flow d'emails sera identique.