10 giorni sull'invisibile: costruire un sistema di email che regga la produzione
10 giorni. 30 commit. Zero nuove funzionalità visibili. Mentre gli utenti di TAMSIV aspettavano un nuovo pulsante, un nuovo colore o una nuova funzione vocale, ho passato ogni giorno a riscrivere qualcosa che nessuno vedrà mai: il modo in cui l'app comunica con le persone via email. E ho capito che l'invisibile forse pesa per l'80% di ciò che fa sì che un prodotto rimanga installato.
Punti chiave da ricordare
- Un sistema completo di email transazionali: promemoria di verifica, feedback loop G+7, preferenze, disiscrizione pulita, storico per utente.
- Un webhook Resend che ascolta i 6 tipi di eventi (delivered, opened, clicked, bounced, complained, delivery_delayed) per aggregare le metriche in tempo reale.
- Un flusso anti-usurpazione RGPD: un utente può segnalare un'email di benvenuto che non ha mai richiesto, l'iscrizione viene eliminata immediatamente.
- Un cron giornaliero che invia un promemoria unico agli account non verificati da 3 giorni, i18n 6 lingue.
- Due hotfix mobile in parallelo (v1.07 + v1.08): audit gesture-handler, riprogettazione UI del feed, modali stabili, 8 bug corretti.
Perché costruire un intero sistema di email quando Supabase Auth invia già le email di verifica
È una domanda legittima. Supabase, nella sua configurazione predefinita, invia già un'email di verifica ad ogni signup. Molti prodotti si fermano qui e non aggiungono altro. Questo è sufficiente per validare un'email, non per costruire una relazione con l'utente.
In produzione dal 4 aprile, mi sono ritrovato con decine di account creati ma non verificati. Nessun sollecito. Nessun feedback a G+7 per sapere se l'app fosse utile. Nessun modo pulito per qualcuno di dire "questa email non era per me". E soprattutto, nessuna visibilità lato admin su ciò che partiva, arrivava, rimbalzava o veniva contrassegnato come spam.
Un sistema di emailing transazionale fatto in casa è esattamente ciò che separa un prodotto "tecnicamente funzionale" da un prodotto che sembra serio. Le grandi app lo nascondono dietro la loro lucidatura. Le piccole app lo trascurano e perdono utenti senza capire perché.
Il cron giornaliero che invia un promemoria, e UNO SOLO
Il primo mattone posato è un cron giornaliero che gira a orario fisso su Vercel. Interroga il database, seleziona gli account creati da esattamente 3 giorni e non ancora verificati, e attiva un promemoria unico per utente.
[ESPERIENZA PERSONALE] La regola "un solo promemoria" è volontaria. Ho ricevuto più email di sollecito abusive di quante ne possa contare. Tre promemoria in 48 ore, cinque in una settimana, dieci in un mese. È il modo migliore per far disiscrivere le persone prima ancora che abbiano provato il prodotto.
Il cron scrive in una tabella dedicata per sapere chi ha ricevuto cosa e quando. Se un utente è già stato sollecitato, viene saltato. Se l'invio Resend fallisce, l'errore viene registrato ma il cron non riprova automaticamente: preferisco una visibilità sui fallimenti a una saturazione silenziosa.
Il feedback loop a G+7: 4 pulsanti, 4 verità
[INSIGHT UNICO] Sette giorni dopo l'iscrizione, ogni nuovo utente riceve un'email con 4 pulsanti cliccabili: mi piace, non mi piace, ho un suggerimento, ho trovato un bug. Ogni pulsante punta a una pagina dedicata che registra la risposta e propone un'area di commento libero.
Perché questa forma precisa? Perché una valutazione su Google Play è filtrata da chi ha più energia per lasciarla. Un'email con 4 pulsanti cattura la verità silenziosa. Quella delle persone che non andranno mai sullo store a scrivere un commento, ma che con un clic possono dire "non mi è piaciuto".
Tecnicamente, ogni pulsante porta un token firmato relativo all'utente e al tipo di feedback. La route GET sul sito valida il token, registra la risposta con l'user_id e il tipo, e visualizza la pagina corrispondente con un modulo libero. Nessuna autenticazione aggiuntiva per lasciare un feedback: è volontario. L'attrito uccide i feedback.
Il flusso anti-usurpazione: RGPD senza drammi
Caso concreto: qualcuno crea un account TAMSIV con l'email alice@esempio.fr. Ma Alice non ha chiesto nulla. Riceve un'email di benvenuto per un account che non ha mai creato. Cosa fa?
Senza questo flusso, mette l'email nello spam o segnala il mittente. In entrambi i casi, è una perdita: perdita per me in termini di reputazione di invio, perdita per lei che non ha un modo pulito per chiudere la questione, perdita per la persona che ha fatto l'errore di battitura e che non riceverà mai più comunicazioni all'indirizzo corretto.
Nella nuova versione, ogni email di benvenuto contiene un link "non ho creato questo account". Clic, pagina dedicata, conferma. L'iscrizione viene immediatamente eliminata dal database, un log viene memorizzato per audit, e un messaggio finale conferma ad Alice che il suo indirizzo non sarà più utilizzato. RGPD senza drammi. Un'azione, tre secondi, è finita.
[DATI ORIGINALI] La pagina è tradotta nelle 6 lingue dell'app (francese, inglese, tedesco, spagnolo, italiano, portoghese). I caratteri accentati erano rotti in alcune lingue a causa di un problema di codifica nel file di traduzione: fix nel commit 12e61a7.
Il webhook Resend: sapere prima che l'utente scriva
Tutti gli invii passano attraverso Resend, un provider di email transazionali focalizzato sull'esperienza dello sviluppatore. Resend offre webhook per ogni evento del ciclo di vita di un'email: email.sent, email.delivered, email.opened, email.clicked, email.bounced, email.complained, email.delivery_delayed.
Tutti vengono ascoltati e memorizzati in una tabella dedicata con il riferimento all'utente, il tipo di email, il timestamp e i metadati associati. Questo permette poi di aggregare: quante email sono arrivate oggi, quante sono state aperte, quali hanno generato un clic, quante sono rimbalzate.
La dashboard admin mostra queste statistiche in tempo reale con badge che si incrementano ad ogni invio. Un bounce che appare su un utente? Lo vedo immediatamente, posso verificare se è un problema temporaneo o un'email morta, e aggiustare. Un complain (segnalazione come spam)? Priorità alta, indagine immediata.
Lo storico degli invii raggruppato per utente, con resend e dedup
L'amministratore vede tutti gli invii recenti, ma raggruppati per utente. Se Alice ha ricevuto la sua email di verifica, poi il suo promemoria G+3, poi il suo feedback G+7, vedo una riga "Alice" con tre sottorighe espandibili. Più leggibile di un flusso cronologico grezzo.
Ogni invio ha un badge "sorgente" (auto / manuale). Gli invii automatici (cron, trigger) sono differenziati dagli invii manuali (pulsante "resend" nell'admin). Un pulsante "reinvia" esiste su ogni riga per i casi in cui un'email è finita nello spam, per forzare un retry su un altro alias, o per testare un cambiamento di template.
Un sistema di dedup impedisce di rispedire spam: se ho inviato un'email di benvenuto 2 ore fa e voglio fare un invio di gruppo "tutte le iscrizioni del giorno", Alice viene esclusa automaticamente perché l'ha già ricevuta. Un badge "idoneo" appare su ogni utente per indicare se può ricevere l'invio corrente o meno.
Due hotfix mobile in parallelo: v1.07 e v1.08
Mentre il backend di emailing progrediva su Vercel, l'app mobile aveva bisogno di aria. Due release sono state rilasciate sul Play Store in Alpha, poi in produzione.
v1.07 (7d65fd0): 8 bug mirati, più una nuova vista compatta nelle cartelle. Quando hai 15 sottocartelle in un progetto, vuoi vedere l'albero a colpo d'occhio senza scorrere. Un toggle passa tra vista dettagliata (schede complete con miniature e anteprime) e vista compatta (righe dense con solo il nome e il conteggio). Zero nuove funzionalità in apparenza, ma un cambiamento d'uso per gli utenti intensivi.
v1.08 (a494e7e): audit completo gesture-handler. TAMSIV usa react-native-gesture-handler ovunque, ma diverse schermate avevano ancora TouchableOpacity o FlatList importati direttamente da react-native. Risultato: tap intermittenti che non passavano, impossibili da riprodurre, segnalati da utenti che finivano per disinstallare senza capire.
L'audit ha toccato più di 25 file. Ogni TouchableOpacity, ogni FlatList, ogni ScrollView è stato spostato sull'import di gesture-handler. Il bug dei tap fantasma è corretto. Approfittane per rifare l'UI del feed e stabilizzare tutte le modali: di passaggio, la lucidatura fa un netto salto di qualità.
Cosa ne ricavo dopo 10 giorni
La cosa che mi ha colpito è che ogni riga di codice scritta durante questi 10 giorni rimarrà invisibile finché funziona. Nessuno dice "wow, la tua email di verifica è arrivata al momento giusto, una sola volta, con la lingua giusta". Nessuno nota che un bounce è stato rilevato automaticamente e che l'admin ha visto il segnale passare prima ancora che l'utente scrivesse.
L'invisibile si nota solo quando fallisce. E a quel punto, l'utente non capisce cosa si è rotto: capisce solo che l'app "non funziona bene". Disinstalla. Non scrive. Non dice perché.
Quindi l'invisibile è forse l'80% di ciò che rende un prodotto finito. Non i pulsanti che aggiungiamo nelle feature release, non i colori che rifacciamo, non le animazioni. Solo il fatto che quando qualcosa deve arrivare all'utente, arriva. Al momento giusto. Una volta. Nella lingua giusta.
Non è sexy da postare su LinkedIn. Ma è ciò che fa sì che un prodotto rimanga installato.
FAQ
Perché non usare un servizio all-in-one come Mailchimp o ConvertKit?
Perché sono strumenti per newsletter, non per transazionali. Sono progettati per invii di gruppo editoriali, non per trigger individuali legati al ciclo di vita dell'utente. Resend è progettato per le email transazionali (benvenuto, verifica, reset password, feedback loop). È lo strumento giusto per il lavoro giusto.
Il webhook Resend è affidabile per gli eventi?
Molto affidabile. Resend ritenta i webhook in caso di fallimento, firma i payload per l'autenticazione ed espone una console per riprodurre manualmente. In 10 giorni di utilizzo, non ho rilevato un singolo evento perso.
Come gestire le traduzioni delle 6 lingue per le email?
Ogni template di email è definito in francese, poi tradotto da uno script automatico tramite OpenRouter (LLM). Le chiavi di traduzione vivono negli stessi file messages/*.json del sito, con un namespace emails dedicato. L'invio lato server prende la lingua preferita dell'utente memorizzata nel database, fallback in inglese se assente.
Il flusso anti-usurpazione è abusabile? Si può eliminare l'account di qualcun altro?
No. Il link "non ho creato questo account" è firmato con un token legato all'indirizzo email di destinazione. Solo la persona che riceve l'email può cliccarci sopra ed eliminare l'iscrizione. Se qualcun altro tenta di chiamare la route senza il token corretto, restituisce un errore 403 senza fare nulla.
Questo sistema si applica a iOS quando l'app verrà rilasciata?
Sì, è backend. Le email partono da Vercel e non dipendono dalla piattaforma dell'app mobile. Il giorno in cui TAMSIV uscirà su iOS (12 persone hanno già cliccato su "Scarica per iOS" sul sito), il flusso di email sarà identico.