Blog
Build in Public
17 avril 202611 min

10 jours sur l'invisible : construire un systeme d'emails qui tient la production

10 jours. 30 commits. Zero nouvelle feature visible. Pendant que les utilisateurs de TAMSIV attendaient un nouveau bouton, une nouvelle couleur ou une nouvelle fonction vocale, j'ai passe chaque journee a reecrire un truc que personne ne verra jamais : la facon dont l'app parle aux gens par email. Et j'ai realise que l'invisible pese peut-etre pour 80% de ce qui fait qu'un produit reste installe.

Points cles a retenir

  • Un systeme d'emails transactionnels complet : rappel de verification, feedback loop J+7, preferences, desinscription propre, historique par user.
  • Un webhook Resend qui ecoute les 6 types d'events (delivered, opened, clicked, bounced, complained, delivery_delayed) pour agreger les metriques en temps reel.
  • Un flow anti-usurpation RGPD : un utilisateur peut signaler un email de bienvenue qu'il n'a jamais demande, on supprime l'inscription immediatement.
  • Un cron quotidien qui envoie un rappel unique aux comptes non verifies depuis 3 jours, i18n 6 langues.
  • Deux hotfix mobile en parallele (v1.07 + v1.08) : audit gesture-handler, refonte UI du feed, modales stables, 8 bugs corriges.

Pourquoi construire un systeme d'emails entier quand Supabase Auth envoie deja les mails de verification

C'est une question legitime. Supabase, dans sa configuration par defaut, envoie deja un email de verification a chaque signup. Beaucoup de produits s'arretent la et n'ajoutent rien d'autre. Ca suffit pour valider un email, pas pour construire une relation avec l'utilisateur.

En production depuis le 4 avril, je me suis retrouve avec des dizaines de comptes crees mais non verifies. Pas de relance. Pas de feedback a J+7 pour savoir si l'app leur etait utile. Pas de moyen propre pour quelqu'un de dire "cet email n'etait pas pour moi". Et surtout, aucune visibilite cote admin sur ce qui partait, arrivait, rebondissait ou etait marque comme spam.

Un systeme d'emailing transactionnel maison, c'est exactement ce qui separe un produit "techniquement fonctionnel" d'un produit qui a l'air serieux. Les grandes apps le cachent derriere leur polish. Les petites apps le negligent et perdent des utilisateurs sans comprendre pourquoi.

Le cron quotidien qui envoie un rappel, et UN SEUL

Le premier brick pose, c'est un cron quotidien qui tourne a heure fixe sur Vercel. Il interroge la base, selectionne les comptes crees depuis exactement 3 jours et non encore verifies, et declenche un rappel unique par utilisateur.

[PERSONAL EXPERIENCE] La regle "un seul rappel" est volontaire. J'ai recu 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 desabonner les gens avant meme qu'ils n'aient essaye le produit.

Le cron ecrit dans une table dediee pour savoir qui a recu quoi et quand. Si un utilisateur est deja relance, il est skip. Si l'envoi Resend echoue, l'erreur est logguee mais le cron ne retente pas automatiquement : je prefere une visibilite sur les echecs a une saturation silencieuse.

Le feedback loop a J+7 : 4 boutons, 4 verites

[UNIQUE INSIGHT] Sept jours apres l'inscription, chaque nouvel utilisateur recoit un email avec 4 boutons cliquables : j'adore, je deteste, j'ai une suggestion, j'ai trouve un bug. Chaque bouton pointe vers une page dediee qui enregistre la reponse et propose une zone de commentaire libre.

Pourquoi cette forme precise ? Parce qu'une note Google Play est filtree par celui qui a le plus d'energie a la laisser. Un email avec 4 boutons capture la verite silencieuse. Celle des gens qui n'iront jamais sur le store ecrire un commentaire, mais qui en un clic peuvent dire "ca ne m'a pas plu".

Techniquement, chaque bouton porte un token signe relatif a l'utilisateur et au type de retour. La route GET sur le site valide le token, enregistre la reponse avec le user_id et le type, et affiche la page correspondante avec un formulaire libre. Pas d'auth supplementaire pour laisser un feedback : c'est volontaire. La friction tue les retours.

Le flow anti-usurpation : RGPD sans drame

Cas concret : quelqu'un cree un compte TAMSIV avec l'email alice@exemple.fr. Mais Alice n'a rien demande. Elle recoit un email de bienvenue pour un compte qu'elle n'a jamais cree. 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 cote reputation 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 cree ce compte". Clic, page dediee, confirmation. L'inscription est immediatement supprimee en base, un log est stocke pour audit, et un message de fin confirme a Alice que son adresse ne sera plus utilisee. RGPD sans drame. Une action, trois secondes, c'est fini.

[ORIGINAL DATA] La page est traduite dans les 6 langues de l'app (francais, anglais, allemand, espagnol, italien, portugais). Les caracteres accentues etaient casses sur certaines langues a cause d'un probleme d'encodage dans le fichier de traduction : fix dans le commit 12e61a7.

Le webhook Resend : savoir avant que l'utilisateur n'ecrive

Tous les envois passent par Resend, un provider d'emails transactionnels focus developer experience. Resend propose des webhooks pour chaque evenement du cycle de vie d'un email : email.sent, email.delivered, email.opened, email.clicked, email.bounced, email.complained, email.delivery_delayed.

Tous sont ecoutes et stockes dans une table dediee avec la reference a l'user, le type d'email, l'horodatage et le metadata associe. Ca permet ensuite d'agreger : combien d'emails sont arrives aujourd'hui, combien ont ete ouverts, lesquels ont genere un clic, combien ont rebondi.

Le dashboard admin affiche ces stats en temps reel avec des badges qui s'incrementent a chaque envoi. Un bounce qui apparait sur un user ? Je le vois immediatement, je peux verifier si c'est un probleme temporaire ou un email mort, et ajuster. Un complain (signalement comme spam) ? Priorite haute, investigation immediate.

L'historique d'envoi groupe par user, avec resend et dedup

L'admin voit tous les envois recents, mais groupes par utilisateur. Si Alice a recu son email de verification 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 differencies des envois manuels (bouton "resend" dans l'admin). Un bouton "renvoyer" existe sur chaque ligne pour les cas ou un email est arrive en spam, pour forcer un retry sur une autre alias, ou pour tester un changement de template.

Un systeme de dedup empeche de re-spammer : si j'ai envoye un email de bienvenue il y a 2 heures et que je veux faire un envoi groupe "toutes les inscriptions du jour", Alice est exclue automatiquement parce qu'elle a deja recu. Un badge "eligible" s'affiche sur chaque user pour indiquer s'il peut recevoir l'envoi en cours ou non.

Deux hotfix mobile en parallele : v1.07 et v1.08

Pendant que le backend emailing avancait 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 cibles, 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'oeil sans scroller. Un toggle bascule entre vue detaillee (cartes completes avec thumbnails et previews) et vue compacte (lignes denses avec juste le nom et le count). Zero 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 ecrans avaient encore des TouchableOpacity ou des FlatList importes depuis react-native directement. Resultat : des taps intermittents qui ne passaient pas, impossible a reproduire, signales par des utilisateurs qui finissaient par desinstaller sans comprendre.

L'audit a touche 25+ fichiers. Chaque TouchableOpacity, chaque FlatList, chaque ScrollView a ete bascule sur l'import de gesture-handler. Le bug des taps fantomes est corrige. 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 apres 10 jours

Le truc qui m'a frappe, c'est que chaque ligne de code ecrite pendant ces 10 jours va rester invisible tant qu'elle marche. Personne ne dit "waouh, ton email de verification est arrive pile au bon moment, une seule fois, avec la bonne langue". Personne ne remarque qu'un bounce a ete detecte automatiquement et que l'admin a vu passer le signal avant meme que l'user n'ecrive.

L'invisible se remarque seulement quand il foire. Et a ce moment-la, l'utilisateur ne comprend pas ce qui a casse : il comprend juste que l'app "ne marche pas bien". Il desinstalle. Il n'ecrit pas. Il ne dit pas pourquoi.

Donc l'invisible, c'est peut-etre 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, ca arrive. Au bon moment. Une fois. Dans la bonne langue.

C'est pas sexy a poster sur LinkedIn. Mais c'est ce qui fait qu'un produit reste installe.

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 concus pour des envois groupes editoriaux, pas pour des triggers individuels lies au cycle de vie de l'utilisateur. Resend est concu pour les emails transactionnels (bienvenue, verification, password reset, feedback loop). C'est le bon outil pour le bon job.

Le webhook Resend est-il fiable pour les events ?

Tres fiable. Resend retente les webhooks en cas d'echec, 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 detecte.

Comment gerer les traductions des 6 langues pour les emails ?

Chaque template d'email est defini en francais, puis traduit par un script automatique via OpenRouter (LLM). Les cles de traduction vivent dans les memes fichiers messages/*.json que le site, avec un namespace emails dedie. L'envoi cote serveur prend la langue preferee de l'utilisateur stockee en base, fallback en anglais si absente.

Le flow anti-usurpation est-il abuse ? Peut-on supprimer le compte de quelqu'un d'autre ?

Non. Le lien "je n'ai pas cree ce compte" est signe avec un token lie a l'adresse email cible. Seule la personne qui recoit 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 systeme s'applique a iOS quand l'app sortira ?

Oui, c'est backend. Les emails partent depuis Vercel et ne dependent pas de la plateforme de l'app mobile. Le jour ou TAMSIV sort sur iOS (12 personnes ont deja clique sur "Telecharger pour iOS" sur le site), le flow d'emails sera identique.