Flusso di consegna delle notifiche
Tracciamento di 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 vive l'auth
- Admin → Express: Cloudflare Access valida l'identità tramite JWT (header
Cf-Access-Jwt-Assertion). Express estrae l'email, controllacrm_user_rolessu supa_new e fa cache per 60s. Le pagine PEOPLE & ROLES richiedonorole='admin'; il composer richiedeadmin|cm. - Express → supa_new:
SUPABASE_SERVICE_ROLE_KEYnell'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. Vero enforcement RLS: ogniSELECT notification_state WHERE user_id=auth.uid()è una garanzia che il client non possa leggere lo stato altrui anche se malicious. - Launcher main → renderer: IPC. La sessione Supabase non passa nel 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 | RTT dell'RPC (~10-20ms) |
| 3 | Express | supa_new | INSERT notification_event | |
| 4 | Express | supa_new | INSERT notification_state (bulk) | una INSERT, N righe |
| 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 dallo step 3).
Poi su ogni launcher dei destinatari:
| # | Frequenza | Operazione |
|---|---|---|
| 6 | 60s + on focus | SELECT notification_state JOIN event (2 RTT in parallelo) |
| 7 | 60s + on focus | SELECT notification_user_prefs |
= 2 RTT per launcher ogni 60s, in parallelo. Su 124 utenti = ~248 query/min a regime quando il 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), enforcement del throttle, validation, chiama supa_new direttamente via service_role | Tiene il service_role lontano dal browser. Centralizza auth/throttle. |
Edge function emit_notification | 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 dovrà 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 | Pipeline di release (CI playroll-installer di Vincenzo) | No — EC-202 |
payment_readiness | Trigger portal su iban_status / documents | No — EC-195 |
validation_outcome | Pipeline di validazione upload | No — EC-195 |
payout_lifecycle | Backend payouts + webhook Wise | No — EC-195 |
annual_cap | Tracker cumulativo backend | Bloccato dal PM (P3) |
EC-195 sarebbe un'edge function aggiuntiva che riceve eventi di dominio dal portal, traduce contributor_id → app_user_id tramite 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 ci 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 focalizzi il launcher). La spec §11.7 M5 prevede una subscription Supabase Realtime 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