Décisions d’Architecture

11 min de lecture

Décisions de Conception Architecturale

Cette page explique le raisonnement derrière les choix technologiques et architecturaux clés de PgArachne. Ces décisions ont été prises pour donner la priorité aux performances, à la sécurité et à la productivité des développeurs, tout en assurant une haute compatibilité avec les agents d’IA et les modèles LLM modernes.

1. Pourquoi PostgreSQL

PgArachne est délibérément construit exclusivement sur PostgreSQL et ne cherche pas à être agnostique vis-à-vis de la base de données. La plupart de ce que la passerelle offre n’est pas une logique Go propre, mais une exploitation directe des fonctionnalités de PostgreSQL.

Pourquoi PostgreSQL :

  • Modèle de permissions intégré : Les rôles, GRANT/REVOKE et les permissions EXECUTE sur les fonctions font partie de la base de données. PgArachne n’a donc pas besoin de sa propre couche d’autorisation — il change simplement de rôle avec SET LOCAL ROLE et PostgreSQL s’occupe du reste. Les règles d’accès s’appliquent ainsi de la même façon à l’API, à psql et à tout autre outil.
  • Row-Level Security : Les politiques au niveau des lignes sont évaluées selon le rôle sous lequel l’appel s’exécute. L’isolation des données entre utilisateurs ou tenants vit dans la base de données, pas dans le code de la passerelle.
  • JSON natif (jsonb) : Le contrat fonction(jsonb) → json correspond exactement au corps de la requête et de la réponse JSON-RPC. Il n’y a ni mapping de paramètres vers des types ni enveloppes à générer — le JSON traverse la passerelle tel quel.
  • Garanties transactionnelles : Chaque appel s’exécute dans une seule transaction avec des garanties ACID complètes. Une fonction peut écrire atomiquement dans plusieurs tables et, en cas d’erreur, tout est annulé sans que la passerelle ait à en savoir quoi que ce soit.
  • Modèle de programmation puissant : PL/pgSQL, les fonctions SQL et d’autres langages procéduraux permettent d’écrire la logique métier là où se trouvent les données, sans transférer de résultats intermédiaires sur le réseau.
  • LISTEN/NOTIFY : Les notifications en temps réel (décision 5) reposent directement sur le mécanisme de PostgreSQL. Aucun message broker externe n’est nécessaire.
  • Introspection du catalogue système : La liste des méthodes appelables, leurs descriptions et les schémas de paramètres sont lus depuis pg_proc et les commentaires de fonctions. capabilities, MCP tools/list et la spécification OpenAPI naissent ainsi d’une seule source de vérité et ne peuvent pas diverger de la réalité.
  • Écosystème d’extensions : PostGIS, pgvector, TimescaleDB, pg_trgm et d’autres extensions sont immédiatement disponibles comme de simples fonctions SQL, donc aussi comme méthodes d’API et outils pour agents d’IA — sans une seule ligne de code Go.
  • Ouverture et maturité : Licence libre sans vendor lock-in, des décennies d’exploitation éprouvée, développement actif et disponibilité chez tous les grands fournisseurs cloud comme en instance auto-hébergée.

Pourquoi pas une autre base de données ou une couche agnostique :

  • Plus petit dénominateur commun : Prendre en charge plusieurs bases de données reviendrait à renoncer précisément aux fonctionnalités sur lesquelles repose PgArachne — le modèle de sécurité par rôles, jsonb, LISTEN/NOTIFY et l’introspection des fonctions. Il ne resterait qu’une couche mince et moins sûre au-dessus d’un SQL générique.
  • MySQL/MariaDB : Ils n’ont ni Row-Level Security native ni équivalent de LISTEN/NOTIFY, et leur prise en charge de JSON et de la logique procédurale est plus limitée.
  • SQL Server et Oracle : Les licences propriétaires et les coûts d’exploitation vont à l’encontre de l’objectif d’un déploiement simple et gratuit avec un seul binaire.
  • Bases NoSQL (documents, clé-valeur, colonnes) : La scalabilité et la flexibilité de schéma pour lesquelles on choisit NoSQL ne sont pas le bénéfice principal pour PgArachne. Le stockage de documents nécessaire est couvert par jsonb, indexation et requêtes comprises, à côté des données relationnelles et dans la même transaction. En revanche, il manque ce sur quoi PgArachne repose : des fonctions serveur appelables avec une permission EXECUTE granulaire pour un rôle donné, la Row-Level Security, l’introspection des fonctions depuis le catalogue et un langage de requête unifié. Sans cela, la passerelle devrait gérer elle-même l’autorisation, la validation et la description de l’API — et cesserait d’être une couche mince.
  • SQLite : C’est une base embarquée sans modèle serveur de rôles et de permissions, sur lequel repose toute la sécurité de PgArachne.

Résultat : PgArachne reste petit. Il délègue la sécurité, les transactions, les notifications et la découverte de l’API à PostgreSQL et ne s’occupe lui-même que de la traduction des protocoles et de l’authentification.

2. Fonctions PostgreSQL comme Surface d’API

PgArachne expose délibérément des fonctions de base de données plutôt que des tables brutes.

Pourquoi les fonctions :

  • Encapsulation : La logique métier réside avec les données dans la base de données — un seul endroit pour auditer, versionner et sécuriser.
  • Sécurité Explicite : Seules les fonctions avec permissions EXECUTE pour un rôle spécifique sont accessibles.
  • Abstraction : Validation, champs calculés et opérations complexes sont masqués pour le client.

Pourquoi pas de CRUD au niveau des tables :

  • Couplage Fort : Expose le schéma interne, rendant la refactorisation difficile sans casser les clients.
  • Fragmentation des Règles Métier : La logique se retrouve divisée entre contraintes de base de données et middleware.

3. Go vs Alternatives

PgArachne est écrit en Go pour offrir le meilleur équilibre entre performances et simplicité de déploiement.

Pourquoi Go :

  • Binaires Statiques : Un seul fichier exécutable sans dépendances externes.
  • Concurrence : Les goroutines gèrent des milliers de connexions simultanées avec légèreté.
  • Bibliothèque Standard Robuste : HTTP, TLS et JSON intégrés, de qualité production.
  • Compilation Croisée : Linux, macOS et Windows (amd64 et arm64) depuis n’importe quelle machine.

Pourquoi pas Node.js, PHP ou Ruby :

  • Runtimes : Nécessitent un environnement d’exécution spécifique sur chaque machine cible.
  • Efficacité : Moins efficaces pour des milliers de connexions SSE inactives.
  • Empreinte Mémoire : Go utilise beaucoup moins de mémoire par connexion.

Pourquoi pas Rust :

  • Vitesse de Développement : Sa complexité ralentit l’itération pour un outil I/O où Go est déjà suffisant.

Pourquoi pas C/C++ :

  • Sécurité : La gestion manuelle de la mémoire ajoute des risques sans gain réel dans une application de passerelle.

4. JSON-RPC 2.0 vs REST

PgArachne utilise JSON-RPC 2.0 comme protocole de communication principal au lieu du traditionnel REST.

Pourquoi JSON-RPC 2.0 :

  • Endpoint Unique : Toute la communication passe par POST /{prefixe}/{base_de_donnees}/jsonrpc. Il n’est pas nécessaire de concevoir des structures d’URL complexes.
  • Appels Auto-contenus : Chaque requête est un objet JSON complet (méthode + paramètres + id), facile à générer et à analyser pour les LLM.
  • Gestion des Erreurs Standardisée : Les codes et messages d’erreur font partie de la spécification.
  • Lotissement (Batching) : Le protocole prend nativement en charge les requêtes par lots en un seul aller-retour HTTP.
  • Découverte (Discovery) : L’endpoint de capacités fournit une description complète de l’API pour les agents d’IA sans hallucinations.

Pourquoi pas REST :

  • Complexité pour l’IA : La sémantique REST est dispersée à plusieurs endroits, rendant difficile la construction d’appels fiables pour les agents d’IA.
  • Fuite de Schéma : Le CRUD sur tables expose souvent la structure interne. PgArachne expose délibérément des fonctions.
  • Absence de Standards : REST n’offre pas de standard universel pour les lots, les enveloppes d’erreurs ou la découverte automatisée.

5. SSE (Server-Sent Events) vs WebSockets

Pour les notifications en temps réel, PgArachne implémente les Server-Sent Events (SSE).

Pourquoi SSE :

  • HTTP Pur : SSE est du HTTP standard, fonctionnant à travers les proxys et CDNs sans configuration spéciale.
  • Support Natif du Navigateur : L’API EventSource gère la reconnexion automatique sans bibliothèques externes.
  • Correspond à la Sémantique NOTIFY : NOTIFY de PostgreSQL est unidirectionnel, ce qui correspond parfaitement au SSE.
  • Multiplexage : Sur HTTP/2, des centaines de flux SSE partagent une seule connexion TCP.
  • Simplicité Opérationnelle : Les connexions SSE apparaissent comme des requêtes HTTP normales dans les logs.

Pourquoi pas WebSockets :

  • Bidirectionnalité Inutile : Le client n’envoie jamais de données via le canal de notification.
  • Problèmes de Connectivité : Souvent bloqués par les pare-feux d’entreprise et certains équilibreurs de charge cloud.
  • Surcharge Plus Élevée : Handshakes et trames ping/pong inutiles pour le simple streaming d’événements.

6. Structure d’URL : /{prefixe}/{base_de_donnees}/{endpoint}

PgArachne route tous les endpoints sous un segment de préfixe configurable : /db/{base_de_donnees}/jsonrpc, /db/{base_de_donnees}/file, /db/{base_de_donnees}/sse, /db/{base_de_donnees}/mcp. Le préfixe par défaut est db et peut être modifié via API_PREFIX.

Pourquoi cette structure :

  • Routage par reverse proxy : Un seul serveur PgArachne peut servir plusieurs bases de données. Un reverse proxy peut router par préfixe ou nom de base de données sans inspecter le corps de la requête, ce qui est essentiel pour l’équilibrage de charge.
  • Scalabilité horizontale : Avec le nom de base de données dans le chemin URL, plusieurs instances PgArachne peuvent être déployées et le trafic dirigé par base de données via des règles de proxy standard, sans sessions persistantes.
  • Multiplexage de protocoles par base de données : Regrouper /jsonrpc, /file, /sse et /mcp sous le même espace de noms /{prefixe}/{base_de_donnees}/ permet d’appliquer l’authentification, le rate limiting et le contrôle d’accès par base de données au niveau du proxy.
  • Préfixe configurable : Les déploiements utilisant déjà /api/ peuvent configurer API_PREFIX=api.
  • Observabilité : Les systèmes de logs et de métriques peuvent grouper le trafic par nom de base de données directement depuis l’URL sans parser les corps JSON.

Pourquoi pas une structure plate comme /api/{base_de_donnees} :

  • Ambiguïté de protocole : Un endpoint plat unique ne peut pas distinguer le trafic JSON-RPC, SSE et MCP au niveau du routage.
  • Plus difficile à étendre : L’ajout de nouveaux protocoles nécessiterait de toute façon de nouvelles routes, donc l’espace de noms structuré prépare la conception pour l’avenir.

7. MCP comme Couche de Traduction, pas comme Protocole de Base de Données

PgArachne implémente le Model Context Protocol (MCP) comme une couche de traduction mince dans le serveur Go. Les fonctions PostgreSQL ne connaissent jamais MCP — elles restent de simples fonctions jsonb → json.

Pourquoi traduire MCP sur le serveur :

  • Aucun changement aux fonctions existantes : Toute fonction déjà exposée via JSON-RPC est instantanément disponible comme outil MCP. Aucun changement SQL.
  • MCP est plus que des outils : Le protocole inclut une méthode de découverte (server/discover), des métadonnées de version de protocole et de capacités transmises à chaque requête, des notifications, ainsi que des extensions (ressources, prompts) au-delà du simple appel d’outils. Ce sont des préoccupations de niveau protocole qui appartiennent à Go, pas aux fonctions SQL.
  • La sécurité reste en un seul endroit : Authentification, changement de rôle et validation sont déjà implémentés en Go. L’endpoint MCP réutilise cette logique inchangée.
  • Plusieurs protocoles, un backend : La même fonction PostgreSQL peut être appelée via JSON-RPC, MCP ou SSE. La base de données est agnostique au protocole.
  • SQL plus simple : Traiter les enveloppes MCP dans PostgreSQL nécessiterait de parser des structures JSON complexes en PL/pgSQL, rendant les fonctions plus difficiles à écrire et maintenir.

Pourquoi ne pas pousser MCP dans la base de données :

  • La validation du transport MCP ne nécessite pas de base de données : server/discover et les vérifications de version de protocole/en-têtes à chaque requête sont des messages de protocole purs. Ouvrir une connexion pour eux gaspille des ressources.
  • SQL est le mauvais outil pour la logique de protocole : Les codes d’erreur JSON-RPC, le routage des notifications et la gestion des clés d’idempotence sont des préoccupations middleware, pas des préoccupations de données.

8. Export OpenAPI : Chemins Virtuels, pas un Nouveau Protocole

GET /{prefixe}/{base_de_donnees}/openapi.json (ou .yaml) génère un document OpenAPI 3.1 décrivant chaque méthode que l’appelant authentifié peut exécuter. L’invocation réelle continue de se faire exclusivement via l’unique endpoint POST /{prefixe}/{base_de_donnees}/jsonrpc — la spécification liste en plus chaque méthode sous son propre chemin, tel que /{prefixe}/{base_de_donnees}/rpc/api.hello_world, mais ce chemin est uniquement de la documentation et n’existe pas comme route HTTP appelable directement.

Pourquoi des chemins virtuels par méthode :

  • Compatibilité avec les outils : Swagger UI, Postman, Insomnia et la plupart des générateurs de code OpenAPI attendent une opération par entrée paths. Un chemin JSON-RPC unique ne peut sinon pas représenter N signatures de méthode différentes d’une manière que ces outils comprennent.
  • Aucun changement backend requis : Ajouter une vraie route par méthode signifierait une seconde façon d’invoquer chaque fonction, avec sa propre surface d’authentification, de gestion des erreurs et de versionnement à maintenir en synchronisation avec JSON-RPC. Générer les chemins uniquement à partir de capabilities() préserve la surface du protocole exactement telle que décrite dans la décision 4, tout en satisfaisant les outils qui ont besoin de schémas par opération.
  • Auto-documenté : La description de chaque opération virtuelle détaille la requête JSON-RPC exacte (méthode + paramètres) nécessaire pour l’appeler réellement, donc rien n’est perdu à ne pas avoir de route réelle.

Pourquoi la spécification est authentifiée et filtrée par rôle :

  • Cohérence avec le reste de l’API : Tous les autres endpoints (JSON-RPC, MCP tools/list) ne révèlent jamais que les méthodes que le rôle de l’appelant peut exécuter. Un document OpenAPI non authentifié et non filtré révélerait l’existence et la forme des paramètres de fonctions qu’un appelant donné ne peut pas réellement invoquer.
  • Le même mécanisme que partout ailleurs : Le gestionnaire Go s’authentifie avec la même logique Basic/JWT/jeton API que /jsonrpc, puis exécute SET LOCAL ROLE avant de générer la spécification — pgarachne.generate_openapi_spec() est SECURITY INVOKER précisément pour que son appel interne à capabilities() voie ce rôle, de la même manière que tools/list de MCP fonctionne déjà.