Passa al contenuto principale

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, controlla crm_user_roles su supa_new e fa cache per 60s. Le pagine PEOPLE & ROLES richiedono role='admin'; il composer richiede admin|cm.
  • Express → supa_new: SUPABASE_SERVICE_ROLE_KEY nell'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. Vero enforcement RLS: ogni SELECT 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":

#DaAOperazioneNote
1Admin browserExpressPOST /notifications/sendl'unico HTTP che l'admin vede
2Expresssupa_newrpc resolve_audienceRTT dell'RPC (~10-20ms)
3Expresssupa_newINSERT notification_event
4Expresssupa_newINSERT notification_state (bulk)una INSERT, N righe
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 dallo step 3).

Poi su ogni launcher dei destinatari:

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

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

CategoriaSource systemBridge esistente?
app_updatePipeline di release (CI playroll-installer di Vincenzo)No — EC-202
payment_readinessTrigger portal su iban_status / documentsNo — EC-195
validation_outcomePipeline di validazione uploadNo — EC-195
payout_lifecycleBackend payouts + webhook WiseNo — EC-195
annual_capTracker cumulativo backendBloccato dal PM (P3)

EC-195 sarebbe un'edge function aggiuntiva che riceve eventi di dominio dal portal, traduce contributor_idapp_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

  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 focalizzi il launcher). La spec §11.7 M5 prevede una subscription Supabase Realtime quando ne avremo bisogno. Per oggi 60s + on-focus è accettabile.

Riferimenti