Decisioni Architetturali

10 min di lettura

Decisioni di Progettazione Architetturale

Questa pagina spiega il ragionamento alla base delle principali scelte tecnologiche e architetturali in PgArachne. Queste decisioni sono state prese per dare priorità a prestazioni, sicurezza e produttività degli sviluppatori, garantendo al contempo un’elevata compatibilità con i moderni agenti AI e modelli LLM.

1. Perché PostgreSQL

PgArachne è costruito deliberatamente solo su PostgreSQL e non cerca di essere agnostico rispetto al database. Gran parte di ciò che offre il gateway non è logica propria in Go, ma l’uso diretto delle funzionalità di PostgreSQL.

Perché PostgreSQL:

  • Modello di permessi integrato: Ruoli, GRANT/REVOKE e permessi EXECUTE sulle funzioni fanno parte del database. PgArachne non ha quindi bisogno di un proprio livello di autorizzazione: cambia semplicemente ruolo con SET LOCAL ROLE e al resto pensa PostgreSQL. Le regole di accesso valgono così allo stesso modo per l’API, per psql e per qualsiasi altro strumento.
  • Row-Level Security: Le policy a livello di riga vengono valutate in base al ruolo con cui avviene la chiamata. L’isolamento dei dati tra utenti o tenant vive quindi nel database, non nel codice del gateway.
  • JSON nativo (jsonb): Il contratto funzione(jsonb) → json corrisponde esattamente al corpo della richiesta e della risposta JSON-RPC. Non servono mapping dei parametri sui tipi né generazione di envelope: il JSON attraversa il gateway invariato.
  • Garanzie transazionali: Ogni chiamata viene eseguita in un’unica transazione con piene garanzie ACID. Una funzione può scrivere atomicamente su più tabelle e, in caso di errore, tutto viene annullato senza che il gateway debba saperne nulla.
  • Modello di programmazione potente: PL/pgSQL, funzioni SQL e altri linguaggi procedurali permettono di scrivere la logica di business dove si trovano i dati, senza trasferire risultati intermedi sulla rete.
  • LISTEN/NOTIFY: Le notifiche in tempo reale (decisione 5) derivano direttamente dal meccanismo di PostgreSQL. Non serve alcun message broker esterno.
  • Introspezione del catalogo di sistema: L’elenco dei metodi richiamabili, le loro descrizioni e gli schemi dei parametri vengono letti da pg_proc e dai commenti delle funzioni. Da un’unica fonte di verità nascono così capabilities, MCP tools/list e la specifica OpenAPI, che non possono divergere dalla realtà.
  • Ecosistema di estensioni: PostGIS, pgvector, TimescaleDB, pg_trgm e altre estensioni sono subito disponibili come normali funzioni SQL, e quindi anche come metodi API e strumenti per agenti AI, senza una sola riga di codice Go.
  • Apertura e maturità: Licenza libera senza vendor lock-in, decenni di esercizio comprovato, sviluppo attivo e disponibilità presso tutti i principali provider cloud oltre che come istanza gestita in autonomia.

Perché non un altro database o un livello agnostico:

  • Minimo comune denominatore: Supportare più database significherebbe rinunciare proprio alle caratteristiche su cui si regge PgArachne: il modello di sicurezza basato sui ruoli, jsonb, LISTEN/NOTIFY e l’introspezione delle funzioni. Resterebbe uno strato sottile e meno sicuro sopra SQL generico.
  • MySQL/MariaDB: Non hanno una Row-Level Security nativa né un equivalente di LISTEN/NOTIFY, e il loro supporto per JSON e logica procedurale è più limitato.
  • SQL Server e Oracle: Licenze proprietarie e costi operativi sono in contrasto con l’obiettivo di un deployment semplice e gratuito con un unico file binario.
  • Database NoSQL (documentali, key-value, a colonne): La scalabilità e la flessibilità dello schema per cui si sceglie NoSQL non sono il vantaggio principale per PgArachne. Lo store documentale necessario è coperto da jsonb, con indicizzazione e interrogazione, accanto ai dati relazionali e nella stessa transazione. Mancano invece gli elementi su cui si basa PgArachne: funzioni lato server richiamabili con permesso EXECUTE granulare per uno specifico ruolo, Row-Level Security, introspezione delle funzioni dal catalogo e un linguaggio di interrogazione unificato. Senza di essi il gateway dovrebbe gestire da sé autorizzazione, validazione e descrizione dell’API, e smetterebbe di essere uno strato sottile.
  • SQLite: È un database embedded senza il modello server di ruoli e permessi su cui è costruita tutta la sicurezza di PgArachne.

Il risultato è che PgArachne resta piccolo: delega a PostgreSQL sicurezza, transazioni, notifiche e scoperta dell’API, e si occupa soltanto della traduzione dei protocolli e dell’autenticazione.

2. Funzioni PostgreSQL come Superficie API

PgArachne espone deliberatamente funzioni del database piuttosto che tabelle grezze.

Perché le funzioni:

  • Incapsulamento: La logica di business reside con i dati nel database — un unico punto per audit, versionamento e sicurezza.
  • Sicurezza Esplicita: Solo le funzioni con permessi EXECUTE per un ruolo specifico sono accessibili.
  • Astrazione: Validazione, campi calcolati e operazioni complesse sono nascosti al client.

Perché no CRUD a livello di tabella:

  • Accoppiamento Stretto: Esporre le tabelle lega l’API allo schema interno, rendendo difficile il refactoring.
  • Frammentazione delle Regole di Business: La logica si divide tra vincoli del database e middleware.

3. Go vs. Alternative

PgArachne è scritto in Go per offrire il miglior equilibrio tra prestazioni e semplicità di distribuzione.

Perché Go:

  • Binari Statici: Un unico file eseguibile senza dipendenze esterne.
  • Concorrenza: Le goroutine gestiscono migliaia di connessioni simultanee in modo leggero.
  • Libreria Standard Robusta: HTTP, TLS e JSON integrati, di livello produttivo.
  • Cross-compilazione: Linux, macOS e Windows (amd64 e arm64) da qualsiasi macchina.

Perché non Node.js, PHP o Ruby:

  • Runtime: Richiedono l’installazione di un ambiente specifico su ogni macchina di destinazione.
  • Efficienza: Meno efficienti per migliaia di connessioni SSE inattive.
  • Impronta di Memoria: Go utilizza molta meno memoria per connessione.

Perché non Rust:

  • Velocità di Sviluppo: La sua complessità rallenta l’iterazione per uno strumento di I/O dove Go è già sufficiente.

Perché non C/C++:

  • Sicurezza: La gestione manuale della memoria aggiunge rischi senza guadagni rilevanti in un’applicazione gateway.

4. JSON-RPC 2.0 vs. REST

PgArachne utilizza JSON-RPC 2.0 come protocollo di comunicazione primario al posto del tradizionale REST.

Perché JSON-RPC 2.0:

  • Endpoint Singolo: Tutte le comunicazioni avvengono tramite POST /{prefisso}/{database}/jsonrpc. Non è necessario progettare strutture URL complesse.
  • Chiamate Autocontenute: Ogni richiesta è un oggetto JSON completo (metodo + parametri + id), facile da generare e analizzare per gli LLM.
  • Gestione degli Errori Standardizzata: I codici e i messaggi di errore fanno parte della specifica.
  • Batching: Il protocollo supporta nativamente le richieste batch in un unico round-trip HTTP.
  • Discovery: L’endpoint delle funzionalità fornisce una descrizione completa dell’API per gli agenti AI senza allucinazioni.

Perché non REST:

  • Complessità per l’AI: La semantica REST è distribuita in più punti, rendendo difficile la costruzione di chiamate affidabili per gli agenti AI.
  • Schema Esposto: Il CRUD sulle tabelle espone spesso la struttura interna. PgArachne espone deliberatamente funzioni.
  • Mancanza di Standard: REST non offre uno standard universale per batch, wrapper di errore o scoperta automatizzata.

5. SSE (Server-Sent Events) vs. WebSockets

Per le notifiche in tempo reale, PgArachne implementa Server-Sent Events (SSE).

Perché SSE:

  • HTTP Puro: SSE è HTTP standard, funziona attraverso proxy e CDN senza configurazioni speciali.
  • Supporto Nativo del Browser: L’API EventSource gestisce la riconnessione automatica senza librerie aggiuntive.
  • Corrisponde alla Semantica NOTIFY: NOTIFY di PostgreSQL è unidirezionale, il che si adatta perfettamente a SSE.
  • Multiplexing: Su HTTP/2, centinaia di stream SSE condividono una singola connessione TCP.
  • Semplicità Operativa: Le connessioni SSE appaiono come normali richieste HTTP nei log.

Perché non WebSockets:

  • Bidirezionalità non Necessaria: Il client non invia mai dati tramite il canale di notifica.
  • Problemi di Connettività: Spesso bloccati da firewall aziendali e alcuni bilanciatori di carico cloud.
  • Maggiore Overhead: Handshake e frame ping/pong inutili per il semplice streaming di eventi.

6. Struttura URL: /{prefisso}/{database}/{endpoint}

PgArachne instrada tutti gli endpoint sotto un segmento di prefisso configurabile: /db/{database}/jsonrpc, /db/{database}/file, /db/{database}/sse, /db/{database}/mcp. Il prefisso predefinito è db e può essere modificato tramite API_PREFIX.

Perché questa struttura:

  • Routing tramite reverse proxy: Un’unica istanza PgArachne può servire più database. Un reverse proxy può instradare per prefisso o nome database senza ispezionare il corpo della richiesta, essenziale per il bilanciamento del carico.
  • Scalabilità orizzontale: Con il nome del database nel percorso URL, è possibile eseguire più istanze PgArachne e instradare il traffico a istanze specifiche tramite regole di proxy standard, senza sessioni persistenti.
  • Multiplexing di protocolli per database: Raggruppare /jsonrpc, /file, /sse e /mcp sotto lo stesso namespace /{prefisso}/{database}/ consente di applicare autenticazione, rate limiting e controllo accessi per database a livello di proxy.
  • Prefisso configurabile: I deployment che usano già /api/ possono impostare API_PREFIX=api.
  • Osservabilità: I sistemi di log e metriche possono raggruppare il traffico per nome database direttamente dall’URL senza analizzare i corpi JSON.

Perché non una struttura piatta come /api/{database}:

  • Ambiguità di protocollo: Un unico endpoint piatto non può distinguere il traffico JSON-RPC, SSE e MCP a livello di routing.
  • Più difficile da estendere: L’aggiunta di nuovi protocolli richiederebbe comunque nuovi percorsi, quindi il namespace strutturato prepara il design per il futuro.

7. MCP come Livello di Traduzione, non come Protocollo di Database

PgArachne implementa il Model Context Protocol (MCP) come un sottile strato di traduzione nel server Go. Le funzioni PostgreSQL non sanno mai di MCP — rimangono semplici funzioni jsonb → json.

Perché tradurre MCP sul server:

  • Nessuna modifica alle funzioni esistenti: Qualsiasi funzione già esposta via JSON-RPC è immediatamente disponibile come tool MCP. Nessuna modifica SQL.
  • MCP è più che soli tool: Il protocollo include un metodo di discovery (server/discover), metadati di versione del protocollo e delle capability su ogni richiesta, notifiche ed estensioni (resources, prompts) oltre alle semplici chiamate ai tool — preoccupazioni a livello di protocollo che appartengono a Go.
  • La sicurezza rimane in un unico posto: Autenticazione, cambio di ruolo e validazione sono già implementati in Go. L’endpoint MCP riutilizza questa logica invariata.
  • Più protocolli, un backend: La stessa funzione PostgreSQL può essere chiamata via JSON-RPC, MCP o SSE. Il database è agnostico al protocollo.
  • SQL più semplice: Elaborare gli envelope MCP (server/discover, tools/list, gestione delle notifiche) in PostgreSQL richiederebbe il parsing di strutture JSON complesse in PL/pgSQL, rendendo le funzioni più difficili da scrivere, testare e mantenere.

Perché non portare MCP nel database:

  • La validazione del transport MCP non necessita del database: server/discover e i controlli di versione del protocollo/header su ogni richiesta sono messaggi di protocollo puri. Aprire una connessione per essi spreca risorse.
  • SQL è lo strumento sbagliato per la logica di protocollo: Codici di errore JSON-RPC, routing delle notifiche e gestione delle chiavi di idempotenza sono preoccupazioni di tipo middleware, non dati.

8. Export OpenAPI: Percorsi Virtuali, non un Nuovo Protocollo

GET /{prefisso}/{database}/openapi.json (o .yaml) genera un documento OpenAPI 3.1 che descrive ogni metodo che il chiamante autenticato può eseguire. L’invocazione effettiva avviene sempre esclusivamente tramite il singolo endpoint POST /{prefisso}/{database}/jsonrpc — la specifica elenca inoltre ogni metodo sotto un proprio percorso, come /{prefisso}/{database}/rpc/api.hello_world, ma tale percorso è puramente documentale e non esiste come route HTTP richiamabile direttamente.

Perché percorsi virtuali per metodo:

  • Compatibilità con gli strumenti: Swagger UI, Postman, Insomnia e la maggior parte dei generatori di codice OpenAPI si aspettano un’operazione per ogni voce in paths. Un singolo percorso JSON-RPC non può altrimenti rappresentare N firme di metodo diverse in un modo comprensibile a questi strumenti.
  • Nessuna modifica al backend necessaria: Aggiungere una route reale per ogni metodo significherebbe un secondo modo di invocare ogni funzione, con una propria superficie di autenticazione, gestione degli errori e versionamento da mantenere in sincrono con JSON-RPC. Generare i percorsi esclusivamente da capabilities() mantiene la superficie del protocollo esattamente come descritta nella decisione 4, pur soddisfacendo gli strumenti che necessitano di schemi per singola operazione.
  • Autodocumentante: La descrizione di ogni operazione virtuale specifica esattamente la richiesta JSON-RPC (metodo + parametri) necessaria per chiamarla realmente, quindi non si perde nulla nel non avere una route reale.

Perché la specifica è autenticata e filtrata per ruolo:

  • Coerenza con il resto dell’API: Ogni altro endpoint (JSON-RPC, MCP tools/list) rivela sempre e solo i metodi che il ruolo del chiamante può eseguire. Un documento OpenAPI non autenticato e non filtrato rivelerebbe l’esistenza e la forma dei parametri di funzioni che un dato chiamante non può effettivamente invocare.
  • Lo stesso meccanismo usato altrove: L’handler Go si autentica con la stessa logica Basic/JWT/token API di /jsonrpc, poi esegue SET LOCAL ROLE prima di generare la specifica — pgarachne.generate_openapi_spec() è SECURITY INVOKER proprio affinché la sua chiamata interna a capabilities() veda quel ruolo, allo stesso modo in cui già funziona MCP tools/list.