Notification delivery flow
Tracing una notifica craftata dall'admin fino alla sua apparizione nel launcher di un destinatario. Lo scopo è capire quanti round-trip ci sono, dove vive l'auth, e quali pezzi sono attivi vs. predisposti-ma-non-ancora-usati.
Lo scenario standard: un admin apre il composer, sceglie destinatari, clicca INVIA. Cosa succede.
Sequence diagram
Dove l'auth vive
- Admin → Express: Cloudflare Access valida l'identità via JWT (
Cf-Access-Jwt-Assertionheader). Express estrae l'email, controllacrm_user_rolessu supa_new e cache-a per 60s. Le pagine PEOPLE & ROLES richiedonorole='admin'; il composer richiedeadmin|cm. - Express → supa_new:
SUPABASE_SERVICE_ROLE_KEYin env del droplet. Bypassa RLS. Mai esposto al browser. - Launcher main → supa_new: la sessione Supabase dell'utente (Steam OAuth),
auth.users.idcorrisponde auser_id. RLS enforcement vero: ogniSELECT notification_state WHERE user_id=auth.uid()è una garanzia che il client non possa leggere stato altrui anche se è malicious. - Launcher main → renderer: IPC. La sessione Supabase non passa in renderer, solo gli snapshot opachi.
Round-trip per send (numero di richieste sulla rete)
Per un singolo "click INVIA":
| # | Da | A | Operazione | Note |
|---|---|---|---|---|
| 1 | Admin browser | Express | POST /notifications/send | l'unico HTTP che l'admin vede |
| 2 | Express | supa_new | rpc resolve_audience | RPC RTT (~10-20ms) |
| 3 | Express | supa_new | INSERT notification_event | |
| 4 | Express | supa_new | INSERT notification_state (bulk) | una INSERT, N rows |
| 5 | Express (opt) | supa_new | INSERT notification_deliveries (bulk) | solo se canali email/push |
= 4-5 round trip Express↔supa_new per ogni send. Tutto in serie perché ogni step dipende dal precedente (event_id viene da step 3).
Poi su ogni launcher dei destinatari:
| # | Frequenza | Operazione |
|---|---|---|
| 6 | 60s + on focus | SELECT notification_state JOIN event (2 RTT in parallel) |
| 7 | 60s + on focus | SELECT notification_user_prefs |
= 2 RTT per launcher ogni 60s, in parallelo. Su 124 utenti = ~248 query/min steady-state quando launcher è aperto. Trascurabile per Postgres ma è il principale costo a regime.
Composer vs. server vs. edge function
Tre pezzi separati, scopi diversi:
| Componente | Cosa fa | Perché esiste |
|---|---|---|
| Composer (browser) | Form, validazione client, preview audience | UX. Nessuna logica di sicurezza qui. |
| Express server | Auth (CF Access + crm_user_roles), throttle enforcement, validation, chiama supa_new direttamente via service_role | Tiene il service_role lontano dal browser. Centralizza auth/throttle. |
emit_notification edge function | Stesso identico body dell'emitNotification() server-side. Accetta Bearer SERVICE_ROLE_KEY o X-Admin-Secret. | Non usata dal composer oggi. Esiste per chiamanti esterni: CI di Vincenzo (EC-202), bridge dal portal (EC-195), futuri integratori. |
L'edge function è quindi codice morto rispetto al flusso composer ma infrastruttura viva per integrazioni future. Vale la pena tenerla in vita perché il giorno che la CI di Vincenzo deve emettere una notifica app_update, basta un curl e non serve aprire le porte del droplet.
Le 3 categorie craftate vs. le 5 programmatiche
Il composer (browser) può emettere solo cohort_announcement, catalog, system. Le altre 5 (payment_readiness, validation_outcome, payout_lifecycle, app_update, annual_cap) sono programmatiche: devono partire dal loro source system.
Lo stato delle 5 programmatiche oggi:
| Categoria | Source system | Bridge esistente? |
|---|---|---|
app_update | Release pipeline (playroll-installer CI di Vincenzo) | No — EC-202 |
payment_readiness | Portal trigger su iban_status / documents | No — EC-195 |
validation_outcome | Pipeline validazione upload | No — EC-195 |
payout_lifecycle | Backend payouts + webhook Wise | No — EC-195 |
annual_cap | Backend cumulative tracker | Blocked by PM (P3) |
EC-195 sarebbe un'edge function aggiuntiva che riceve eventi domain dal portal, traduce contributor_id → app_user_id via contributor_app_links, costruisce il payload notifica, e chiama emit_notification. Quindi emit_notification resta il punto di scrittura unico; EC-195 è il traduttore che sta sopra.
Inefficienze identificate
- Doppia
resolve_audiencedurante un send sereno: il browser fa audience-preview (step ~3), poi il send rifàresolve_audience(step 2 del send). Se l'audience è grande e l'utente clicca Send entro pochi secondi, è lavoro ripetuto. Mitigation possibile: il preview ritorna unaudience_fingerprintche il send riusa per validare che non sia cambiato. Non urgente — il costo è ~10-50ms per send, e il send è raro (decine al giorno al massimo). - Polling launcher 60s + 2 query separate ogni volta: invece di tenere 2 query separate (state JOIN event + prefs), si potrebbe fare una RPC
fetch_notifications_for_userche ritorna entrambe in 1 RTT. Per 124 utenti la differenza è 124 RTT/min risparmiati. Non urgente. - Nessun realtime (websocket): il polling 60s introduce latenza fino a 1 minuto tra send e visione (a meno che l'utente non focusi il launcher). Spec §11.7 M5 prevede Supabase Realtime subscription quando ne avremo bisogno. Per oggi 60s + on-focus è accettabile.
Riferimenti
- Spec sistema notifiche (playroll-cpp)
- Notifications composer ops — runbook per chi usa la UI
- People & Roles ops — chi può accedere a cosa