Files
CRAPP/docs/decisions/DD-036.md
T
davideandClaude Sonnet 5.5 4c8791fa2c Docs: file di supporto in ARCHITECTURE e riga Codice nelle decisioni
Cita con percorso completo i 28 file di src/ che nessun documento nominava e
aggiunge la riga Codice a 21 decisioni, ricavata dai file che le citano.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-15 18:55:46 +01:00

78 lines
5.0 KiB
Markdown

# DD-036 — Notifiche email via Gmail, da un worker Docker sull'host di casa
**Data:** 29 settembre 2026
**Stato:** Accettata
**Codice:** `mailer/mailer-core.ts`, `mailer/mailer.mjs`, `src/lib/preferenze-utente.ts`, `supabase/migrations/20260929120000_m22_notifiche_email.sql`
**Contesto**
Le notifiche di CrAPP arrivano solo con la push (che dipende da Chrome vivo in background,
vedi i limiti in `docs/modules/notifiche.md`) e nel centro notifiche in-app, che si vede solo
aprendo l'app. Chi non ha attivato la push o ha un telefono aggressivo sul risparmio
energetico non riceve nulla ad app chiusa. Serve un canale che arrivi comunque, **gratis**, ai
giocatori registrati, che sono tutti su Gmail perché l'accesso è con Google. L'app gira su
Vercel, che non esegue container né processi sempre attivi.
**Decisione**
Le email partono da un **worker Docker sull'host di casa** che legge una coda su Supabase e
invia tramite `smtp.gmail.com` con un account Gmail dedicato e una password per app.
- La coda è `notifiche_email_coda`, riempita da un trigger su `notifiche_utente`: ogni
notifica già esistente, di qualunque tipo e per qualunque destinatario (admin compresi,
con la stessa logica degli altri), diventa anche una mail. Le sorgenti di M17 non cambiano,
non ne nascono di nuove e l'app non conosce le email. Il certificato in scadenza resta
fuori: è una card in Home, non una notifica (DD-035).
- Il worker fa solo polling **in uscita** con la service role: nessuna porta aperta sull'host.
- La coda è una tabella a parte, con RLS senza policy, perché la policy `UPDATE` di
`notifiche_utente` lascerebbe a un client la possibilità di rimettere in coda una mail.
- Canale **opt-out**: acceso di default per tutti, con un interruttore per account in
Profilo → Opzioni. La preferenza sta in una tabella nuova, `preferenze_utente`, una riga
per account (non per slot); l'assenza di riga vale «acceso».
- Specifica completa in [notifiche-email.md](../modules/notifiche-email.md); installazione e
gestione in [WORKER_EMAIL.md](../WORKER_EMAIL.md).
**Alternative scartate**
- Inviare direttamente dal proprio IP con un MTA (Postfix) → scartata: porta 25 bloccata dai
provider residenziali, IP nelle blocklist, niente reverse DNS; le mail finirebbero in spam o
sarebbero rifiutate.
- Relay Brevo o Resend → scartati per ora: il piano gratuito funziona ma per una buona
consegna richiede un dominio con SPF/DKIM/DMARC, che oggi non c'è. Restano l'alternativa
se servirà un mittente della squadra.
- Supabase Edge Function con `pg_cron` e un relay → scartata su richiesta esplicita di un
servizio dockerizzato sull'host; resta la strada senza host acceso, ma dipende comunque da un
relay esterno.
- Vercel Cron → scartato: sul piano gratuito gira al massimo una volta al giorno, troppo poco
per i promemoria a 24h e 3h.
- Chiamare l'host dall'app (webhook verso casa) → scartato: richiederebbe un IP pubblico o un
tunnel e una superficie d'attacco in più; il polling in uscita non ne ha.
- Colonne di stato sulla riga di `notifiche_utente` → scartate per la RLS spiegata sopra.
**Conseguenze**
- Costo zero, ma il canale email dipende da un host acceso: se è spento le mail arrivano in
ritardo (mai perse). Consegna _almeno una volta_: in caso di crash tra invio e
registrazione può arrivare un doppione.
- Il tetto di circa 500 destinatari al giorno di Gmail è largo per una squadra, ma è un limite
reale: con molte squadre o riepiloghi frequenti andrebbe rivisto.
- Nuova migration (`m22_notifiche_email`), nuova cartella `mailer/` (worker bun con la sola
dipendenza `nodemailer`, separato dall'app) e un segreto in più (la service role sull'host).
L'app non guadagna nessuna dipendenza e le route di M17 non cambiano.
- Il certificato in scadenza resta fuori dalle email: non è una notifica (DD-035), quindi non
passa da `notifiche_utente`. Renderlo una notifica sarebbe una decisione a parte.
- Il tetto giornaliero è una finestra mobile di 24 ore, `MAIL_LIMITE_GIORNO` (400 di default,
sotto i circa 500 di Gmail).
- Le email dei giocatori si usano per un secondo scopo oltre al login: da qui l'opt-out e
l'indicazione in ogni mail su come disattivarlo.
- Una tabella in più, `preferenze_utente`, pensata per accogliere in futuro anche le
preferenze per tipo di notifica senza toccare `giocatori_squadra`.
- In produzione dal 29/09/2026 (migration e worker su un Raspberry Pi). Dalla prova reale: la
prima mail, da un account Gmail nuovo, è arrivata ma nella cartella spam, e la password per app
non era disponibile finché l'account aveva le sole passkey come secondo passaggio (dettagli in
[WORKER_EMAIL.md](../WORKER_EMAIL.md#account-gmail-e-password-per-app)).
**Riesame**
Se serve un mittente col dominio della squadra, se le mail finiscono in spam anche dopo qualche
giorno di uso, se si supera il tetto giornaliero di Gmail, o se l'host di casa si rivela poco
affidabile: passare a un relay con dominio e, in quel caso, valutare Edge Function più `pg_cron`
al posto del container.