Quando un'applicazione necessita di aggiornamenti in tempo reale, la risposta predefinita nel 2025 è un servizio gestito: Firebase Realtime Database, Supabase Realtime, Pusher, Ably o un livello WebSocket ospitato. Questi servizi astraggono la complessità dell'invio di aggiornamenti ai client collegati. Funzionano bene, finché non smettono.
In Atlas Verified abbiamo iniziato con una piattaforma gestita di banca dati che includeva sottoscrizioni in tempo reale. Crescendo, abbiamo raggiunto i limiti di quell'astrazione. Vincoli del raggruppamento delle connessioni, problemi di affidabilità in tempo reale, necessità di interrogazioni spaziali PostGIS e desiderio di logiche di trigger personalizzate ci hanno spinto nella stessa direzione: controllare il livello della banca dati.
Siamo passati a PostgreSQL ospitato autonomamente e abbiamo costruito il sistema in tempo reale utilizzando il meccanismo nativo LISTEN/NOTIFY con Server-Sent Events. Questo articolo spiega perché abbiamo deciso così, come funziona l'architettura e cosa abbiamo imparato.
Perché abbiamo lasciato la piattaforma gestita
Le piattaforme gestite offrono una proposta interessante: autenticazione, sicurezza a livello di riga, sottoscrizioni in tempo reale, API generate automaticamente e dashboard, tutto pronto all'uso. Per prodotti nelle fasi iniziali è davvero utile. Puoi costruire un'applicazione funzionante senza scrivere una sola riga di codice backend.
I problemi emergono quando l'applicazione matura e i requisiti divergono dalle ipotesi della piattaforma.
Limiti delle connessioni. Le piattaforme gestite spesso instradano le connessioni attraverso un gestore del pool che impone limiti propri. Con più servizi backend che richiedono pool propri, un server API, un sistema di elaborazione in background e un listener in tempo reale, esaurisci rapidamente le connessioni disponibili. Ci siamo ritrovati ad aggirare i limiti del gestore anziché risolvere veri problemi del prodotto.
Affidabilità in tempo reale. Il sistema in tempo reale della piattaforma gestita operava tramite Change Data Capture sul Write-Ahead Log di PostgreSQL. È elegante sul piano architetturale, ma introduceva guasti che non potevamo controllare. La replica basata su WAL ha una latenza intrinseca. Il servizio talvolta restava indietro, consegnando aggiornamenti obsoleti. La riconnessione dopo interruzioni di rete era inaffidabile. E analizzare questi problemi richiedeva accesso a infrastrutture che non controllavamo.
Requisiti delle estensioni. La nostra applicazione elabora documenti commerciali con dati geografici: località portuali, rotte e posizioni delle navi. PostGIS, l'estensione spaziale di PostgreSQL, è essenziale per queste interrogazioni. La piattaforma gestita lo supportava, ma ci servivano versioni e configurazioni specifiche non sempre disponibili.
Logica personalizzata dei trigger. Ci servivano trigger della banca dati che attivassero eventi specifici dell'applicazione, oltre a generiche notifiche di «riga modificata». Quando cambia lo stato di elaborazione di un documento, dobbiamo avvisare lo specifico client collegato che lo ha caricato. Il sistema gestito operava a livello di tabella; ci serviva il livello di entità.
La decisione di migrare non riguardava costi o principi. Era pragmatica: dedicavamo più tempo ad aggirare i vincoli della piattaforma di quanto avremmo impiegato a costruire le funzionalità autonomamente.
LISTEN/NOTIFY: il pub/sub nascosto di PostgreSQL
PostgreSQL include un meccanismo integrato di pubblicazione/sottoscrizione dalla versione 9.0, rilasciata nel 2010. Nonostante abbia oltre 15 anni, LISTEN/NOTIFY è sorprendentemente poco utilizzato. Molti sviluppatori non sanno che esiste.
Il meccanismo è semplice. Qualsiasi connessione può eseguire LISTEN su un canale denominato per sottoscriverlo. Qualsiasi connessione può eseguire NOTIFY per inviare un messaggio a tutti i listener del canale. Il payload è una stringa limitata a 8,000 byte.
Cosa rende LISTEN/NOTIFY potente per le applicazioni in tempo reale:
Nessun onere infrastrutturale aggiuntivo. Non esiste un broker di messaggi separato da distribuire, monitorare o scalare. Se hai PostgreSQL, hai pub/sub. Per questo caso specifico non servono Redis, RabbitMQ o Kafka.
Garanzie transazionali. I messaggi NOTIFY vengono consegnati soltanto quando la transazione che li contiene viene confermata. Se una transazione inserisce una riga e invia una notifica, poi viene annullata, la notifica non viene mai consegnata. È una proprietà che i broker esterni non offrono senza un attento coordinamento.
Integrazione dei trigger. I trigger PostgreSQL possono chiamare pg_notify durante l'esecuzione. La banca dati può quindi avvisare automaticamente i listener quando cambiano dati specifici, senza codice applicativo nel percorso di scrittura.
I limiti sono reali e meritano di essere compresi:
Nessuna persistenza. Se nessuno ascolta quando viene eseguito NOTIFY, il messaggio è perso. Non ci sono riproduzione, coda di messaggi o garanzie di consegna per client scollegati. I listener devono gestire riconnessione e recupero dello stato.
Dimensione del payload. Il limite di 8,000 byte impedisce di inviare oggetti grandi tramite NOTIFY. Inviamo payload minimi, nome della tabella, tipo di operazione e chiave primaria, lasciando al client il recupero dei dati completi se necessario.
Ambito di una sola banca dati. LISTEN/NOTIFY funziona all'interno di una singola istanza PostgreSQL. Non si estende a repliche o cluster. Per architetture in cui tutte le scritture vanno a un solo nodo primario, non è un limite.
La nostra architettura: trigger, SSE e multiplexing dei canali
Ecco come si collegano i componenti.
I trigger della banca dati sono installati sulle tabelle in cui contano gli aggiornamenti in tempo reale: documenti, messaggi, conversazioni, notifiche, verifiche delle spedizioni e altre. Quando una riga viene inserita, aggiornata o eliminata, il trigger chiama pg_notify con un payload JSON contenente nome della tabella, tipo di operazione, chiave primaria ed eventuali colonne di filtro.
Una connessione listener dedicata nel server API esegue LISTEN sul canale di notifica e riceve tutti gli eventi dei trigger. È una connessione PostgreSQL dedicata e duratura, separata dal pool delle interrogazioni, che si limita ad ascoltare le notifiche.
Un bus di eventi riceve le notifiche dal listener e le indirizza ai client collegati in base alle sottoscrizioni. Un client che sottoscrive documenti con un determinato ID di spedizione riceve soltanto notifiche sui documenti di quella spedizione.
Server-Sent Events consegna le notifiche al browser. Ogni client collegato apre una sola connessione SSE al server API, specificando canali e filtri desiderati. Il bus confronta le notifiche in ingresso con le sottoscrizioni e invia gli eventi corrispondenti sulle connessioni SSE appropriate.
Perché SSE anziché WebSockets
È una decisione che ci ha sempre soddisfatto. Server-Sent Events è più semplice di WebSockets in ogni aspetto rilevante per il nostro caso d'uso.
Unidirezionale per progettazione. Le nostre esigenze in tempo reale sono dal server al client: il server comunica quando cambiano i dati. Il client non deve inviare messaggi in tempo reale al server, perché utilizza richieste HTTP standard. Il modello unidirezionale SSE corrisponde al requisito effettivo.
Nativo HTTP. Le connessioni SSE sono richieste HTTP standard con un tipo di contenuto specifico. Funzionano attraverso ogni proxy HTTP, bilanciatore e CDN senza configurazione speciale. WebSockets richiede HTTP Upgrade, che alcuni proxy gestiscono male e alcuni firewall aziendali bloccano completamente.
Riconnessione automatica. L'API EventSource del browser gestisce nativamente la riconnessione. Se la connessione cade, il browser si ricollega automaticamente con attesa esponenziale. Con WebSockets devi implementare tu la logica e sbagliarla almeno una volta.
L'unico ambito in cui WebSockets vince, la comunicazione bidirezionale, semplicemente non ci serve. Per applicazioni che richiedono messaggi in tempo reale dal client al server, WebSockets è la scelta giusta. Per notifiche dal server al client, lo è SSE.
Costruire un livello di interrogazione senza ORM
Lasciando la piattaforma gestita, abbiamo lasciato anche la sua API di interrogazione generata automaticamente. Ci serviva un livello di accesso alla banca dati per il server API.
Le opzioni standard sono ORM come Prisma, TypeORM o Drizzle, oppure SQL diretto con un driver. Abbiamo scelto una terza strada: un generatore leggero di interrogazioni con API concatenabili che genera ed esegue SQL diretto sottostante.
API familiare. Il codice esistente utilizzava la libreria client della piattaforma gestita. Un'API compatibile significava modifiche minime nella migrazione. Le interrogazioni che passavano dal client della piattaforma potevano essere eseguite dal nostro generatore con sintassi identica.
Nessuna magia. Gli ORM generano SQL che non puoi vedere e talvolta controllare. Quando un'interrogazione è lenta, devi leggere l'SQL generato, che spesso assomiglia poco alla chiamata ORM originale. Il nostro generatore produce SQL prevedibile: ogni metodo concatenato corrisponde a una specifica clausola SQL e puoi sempre esaminare l'interrogazione generata.
Join incorporati. I dati commerciali sono relazionali. Una spedizione ha documenti, che hanno nodi, che hanno controlli di verifica. Esprimere selezioni annidate e ricevere un risultato annidato, senza scrivere ogni volta manualmente un JOIN complesso, migliora significativamente l'esperienza dello sviluppatore.
Il risultato è un livello di interrogazione di poche centinaia di righe, che gestisce la grande maggioranza delle interazioni con la banca dati e produce SQL sempre leggibile e ottimizzabile.
Gestione del budget del pool di connessioni
Ospitando autonomamente PostgreSQL, controlli il budget delle connessioni. L'impostazione max_connections definisce il limite assoluto e ogni connessione consuma memoria.
Abbiamo scoperto che considerare le connessioni come un budget, una risorsa finita condivisa tra servizi, previene problemi che colpiscono i team senza pianificazione.
Ogni servizio dichiara il massimo delle connessioni. La somma deve restare molto sotto il limite PostgreSQL, con margine per connessioni occasionali: sessioni psql, script di migrazione, monitoraggio. Sembra ovvio, ma molti team lo fanno soltanto quando esauriscono le connessioni in produzione. Pianificando in anticipo, abbiamo evitato quella particolare chiamata alle 3 del mattino.
Migrazioni sicure su larga scala
Passare dalla piattaforma gestita a PostgreSQL autonomo significava spostare centinaia di tabelle, funzioni, trigger e indici. Ma la sfida maggiore era costruire un sistema di migrazione che funzionasse anche durante l'evoluzione dell'applicazione.
Il nostro sistema automatico funziona così: file di migrazione denominati con marcature temporali in una directory dedicata. Un esecutore esamina la directory, confronta una tabella che registra le migrazioni applicate ed esegue quelle in attesa nell'ordine corretto. Ogni migrazione opera in una transazione: se fallisce, viene annullata completamente e blocca la distribuzione finché il problema non è risolto.
L'esecutore parte automaticamente a ogni distribuzione, prima dell'applicazione. Le modifiche allo schema vengono quindi applicate nel processo di distribuzione, senza passaggi manuali. Uno sviluppatore crea un file SQL con marcatura temporale, lo invia al repository e la distribuzione successiva lo applica.
Decisioni principali per la sicurezza: le migrazioni utilizzano controlli idempotenti per non fallire se applicate parzialmente. Ogni migrazione deve essere uno script SQL autonomo eseguibile senza intervento umano. Se fallisce, la transazione viene interamente annullata: l'applicazione non parte con una modifica dello schema parzialmente applicata.
Cosa abbiamo ottenuto
Passare a PostgreSQL autonomo con sistema personalizzato in tempo reale è stato un investimento ingegneristico significativo. Ma i benefici sono stati notevoli:
Affidabilità. Le notifiche arrivano entro millisecondi dalla modifica dei dati sottostanti. Nessun ritardo di replica WAL, nessuna indisponibilità di servizi di terzi nel percorso critico.
Facilità di analisi dei problemi. Quando qualcosa va storto, possiamo seguire l'intero percorso: attivazione del trigger, invio della notifica, instradamento del bus, consegna SSE. Ogni passaggio è nel nostro codice, nei nostri registri e sulla nostra infrastruttura.
Flessibilità. Aggiungere aggiornamenti in tempo reale a una nuova tabella richiede una sola migrazione SQL che installa un trigger. Nessuna configurazione della piattaforma, dashboard di terzi o richiesta di assistenza.
Prestazioni. Le connessioni PostgreSQL dirette con un pool ottimizzato superano quelle instradate nell'infrastruttura gestita. La latenza delle interrogazioni è diminuita sensibilmente dopo la migrazione.
Il compromesso è la responsabilità operativa. Gestiamo la banca dati, i backup, il monitoraggio della replica e la capacità. Per un team esperto nelle operazioni PostgreSQL è gestibile. Per uno meno esperto, le astrazioni della piattaforma gestita potrebbero valere i relativi limiti.
Atlas Verified opera su PostgreSQL autonomo con infrastruttura personalizzata in tempo reale, elaborando quotidianamente migliaia di verifiche di conformità commerciale lungo la filiera globale.
