- Spezza il registro delle decisioni in docs/decisions/DD-XXX.md, con indice - Ripulisce DATABASE.md dalla storia delle migration e aggiunge la sezione Lettura - Riduce PROJECT_STATE.md a una pagina; la procedura di allestimento passa in OPERATIONS.md - Aggiunge OPERATIONS.md, GLOSSARIO.md, trappole note e checklist in ARCHITECTURE.md - Sintetizza la copertura test di badge.md e documenta docs/archive - Allinea AGENTS.md, README.md e la regola Cursor (tolto l'obbligo di develop, in contrasto con DD-019) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
4.8 KiB
DD-036 — Notifiche email via Gmail, da un worker Docker sull'host di casa
Data: 29 settembre 2026
Stato: Accettata
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 sunotifiche_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
UPDATEdinotifiche_utentelascerebbe 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; installazione e gestione in 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_crone 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 cartellamailer/(worker bun con la sola dipendenzanodemailer, 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 toccaregiocatori_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).
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.