Skip to main content

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-Assertion header). Express estrae l'email, controlla crm_user_roles su supa_new e cache-a per 60s. Le pagine PEOPLE & ROLES richiedono role='admin'; il composer richiede admin|cm.
  • Express → supa_new: SUPABASE_SERVICE_ROLE_KEY in env del droplet. Bypassa RLS. Mai esposto al browser.
  • Launcher main → supa_new: la sessione Supabase dell'utente (Steam OAuth), auth.users.id corrisponde a user_id. RLS enforcement vero: ogni SELECT 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":

#DaAOperazioneNote
1Admin browserExpressPOST /notifications/sendl'unico HTTP che l'admin vede
2Expresssupa_newrpc resolve_audienceRPC RTT (~10-20ms)
3Expresssupa_newINSERT notification_event
4Expresssupa_newINSERT notification_state (bulk)una INSERT, N rows
5Express (opt)supa_newINSERT 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:

#FrequenzaOperazione
660s + on focusSELECT notification_state JOIN event (2 RTT in parallel)
760s + on focusSELECT 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:

ComponenteCosa faPerché esiste
Composer (browser)Form, validazione client, preview audienceUX. Nessuna logica di sicurezza qui.
Express serverAuth (CF Access + crm_user_roles), throttle enforcement, validation, chiama supa_new direttamente via service_roleTiene il service_role lontano dal browser. Centralizza auth/throttle.
emit_notification edge functionStesso 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:

CategoriaSource systemBridge esistente?
app_updateRelease pipeline (playroll-installer CI di Vincenzo)No — EC-202
payment_readinessPortal trigger su iban_status / documentsNo — EC-195
validation_outcomePipeline validazione uploadNo — EC-195
payout_lifecycleBackend payouts + webhook WiseNo — EC-195
annual_capBackend cumulative trackerBlocked 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

  1. Doppia resolve_audience durante 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 un audience_fingerprint che 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).
  2. 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_user che ritorna entrambe in 1 RTT. Per 124 utenti la differenza è 124 RTT/min risparmiati. Non urgente.
  3. 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