Decisões de Arquitetura

10 min de leitura

Decisões de Design Arquitetônico

Esta página explica o raciocínio por trás das principais escolhas tecnológicas e arquiteturais do PgArachne. Estas decisões foram tomadas para priorizar performance, segurança e produtividade do desenvolvedor, garantindo ao mesmo tempo alta compatibilidade com agentes de IA e modelos LLM modernos.

1. Por que PostgreSQL

O PgArachne é construído deliberadamente apenas sobre o PostgreSQL e não tenta ser agnóstico em relação ao banco de dados. A maior parte do que o gateway oferece não é lógica própria em Go, mas o uso direto de recursos do PostgreSQL.

Por que PostgreSQL:

  • Modelo de permissões integrado: Papéis, GRANT/REVOKE e permissões EXECUTE em funções fazem parte do banco de dados. Por isso, o PgArachne não precisa de uma camada de autorização própria — ele apenas troca o papel com SET LOCAL ROLE e o PostgreSQL cuida do resto. As regras de acesso valem, assim, da mesma forma para a API, o psql e qualquer outra ferramenta.
  • Row-Level Security: As políticas em nível de linha são avaliadas de acordo com o papel em nome do qual a chamada é executada. O isolamento de dados entre usuários ou tenants vive no banco de dados, e não no código do gateway.
  • JSON nativo (jsonb): O contrato função(jsonb) → json corresponde exatamente ao corpo da requisição e da resposta JSON-RPC. Não é preciso mapear parâmetros para tipos nem gerar envelopes — o JSON atravessa o gateway sem alterações.
  • Garantias transacionais: Cada chamada é executada em uma única transação com garantias ACID completas. Uma função pode gravar atomicamente em várias tabelas e, em caso de erro, tudo é revertido, sem que o gateway precise saber de nada.
  • Modelo de programação poderoso: PL/pgSQL, funções SQL e outras linguagens procedurais permitem escrever a lógica de negócio onde estão os dados, sem transferir resultados intermediários pela rede.
  • LISTEN/NOTIFY: As notificações em tempo real (decisão 5) vêm diretamente do mecanismo do PostgreSQL. Não é necessário um message broker externo.
  • Introspecção do catálogo do sistema: A lista de métodos chamáveis, suas descrições e os esquemas de parâmetros são lidos de pg_proc e dos comentários das funções. De uma única fonte da verdade surgem capabilities, o tools/list do MCP e a especificação OpenAPI, que não podem divergir da realidade.
  • Ecossistema de extensões: PostGIS, pgvector, TimescaleDB, pg_trgm e outras extensões ficam imediatamente disponíveis como funções SQL comuns e, portanto, também como métodos da API e ferramentas para agentes de IA — sem uma única linha de código Go.
  • Abertura e maturidade: Licença livre sem vendor lock-in, décadas de operação comprovada, desenvolvimento ativo e disponibilidade em todos os principais provedores de nuvem, e também como instância autogerenciada.

Por que não outro banco de dados ou uma camada agnóstica:

  • Menor denominador comum: Suportar vários bancos de dados significaria abrir mão justamente dos recursos sobre os quais o PgArachne se apoia — o modelo de segurança por papéis, o jsonb, o LISTEN/NOTIFY e a introspecção de funções. Sobraria uma camada fina e menos segura sobre SQL genérico.
  • MySQL/MariaDB: Não têm Row-Level Security nativo nem equivalente ao LISTEN/NOTIFY, e o suporte a JSON e a lógica procedural são mais limitados.
  • SQL Server e Oracle: O licenciamento proprietário e os custos operacionais contradizem o objetivo de uma implantação simples e gratuita com um único binário.
  • Bancos NoSQL (documentos, chave-valor, colunares): A escalabilidade e a flexibilidade de esquema pelas quais se escolhe NoSQL não são o principal benefício para o PgArachne. O armazenamento de documentos necessário é coberto pelo jsonb, incluindo indexação e consultas, ao lado dos dados relacionais e na mesma transação. Em contrapartida, falta justamente aquilo em que o PgArachne se apoia: funções no servidor chamáveis com permissão granular EXECUTE para um papel específico, Row-Level Security, introspecção de funções a partir do catálogo e uma linguagem de consulta unificada. Sem isso, o gateway teria de resolver sozinho a autorização, a validação e a descrição da API — e deixaria de ser uma camada fina.
  • SQLite: É um banco de dados embarcado sem modelo de servidor com papéis e permissões, no qual toda a segurança do PgArachne é construída.

Como consequência, o PgArachne continua pequeno: delega ao PostgreSQL a segurança, as transações, as notificações e a descoberta da API e cuida apenas da tradução de protocolos e da autenticação.

2. Funções PostgreSQL como Superfície da API

O PgArachne expõe deliberadamente funções do banco de dados em vez de tabelas diretamente.

Por que funções:

  • Encapsulamento: A lógica de negócio reside com os dados no banco de dados — um único lugar para auditoria, versionamento e segurança.
  • Segurança explícita: Apenas as funções com permissões EXECUTE para um papel específico são acessíveis.
  • Abstração: Validação, campos calculados e operações complexas ficam ocultos para o cliente.

Por que não CRUD em nível de tabela:

  • Acoplamento forte: Expor tabelas vincula a API ao esquema interno, dificultando refatorações.
  • Fragmentação de regras de negócio: A lógica se divide entre restrições do banco de dados e middleware.

3. Go vs. Alternativas

O PgArachne é escrito em Go para oferecer o melhor equilíbrio entre performance e simplicidade de implantação.

Por que Go:

  • Binários estáticos: Um único arquivo executável sem dependências externas.
  • Concorrência: As goroutines gerenciam milhares de conexões simultâneas de forma eficiente.
  • Biblioteca padrão robusta: HTTP, TLS e JSON integrados, de nível de produção.
  • Compilação cruzada: Linux, macOS e Windows (amd64 e arm64) a partir de qualquer máquina.

Por que não Node.js, PHP ou Ruby:

  • Runtimes: Requerem a instalação de um ambiente específico em cada máquina de destino.
  • Eficiência: Menos eficientes para manter milhares de conexões SSE inativas.
  • Consumo de memória: O Go utiliza significativamente menos memória por conexão.

Por que não Rust:

  • Velocidade de desenvolvimento: A sua complexidade retarda a iteração para uma ferramenta de I/O onde o Go já é suficiente.

Por que não C/C++:

  • Segurança: O gerenciamento manual de memória adiciona riscos sem ganhos relevantes em uma aplicação de gateway.

4. JSON-RPC 2.0 vs. REST

O PgArachne utiliza JSON-RPC 2.0 como seu protocolo de comunicação primário em vez do tradicional REST.

Por que JSON-RPC 2.0:

  • Endpoint único: Toda a comunicação ocorre via POST /{prefixo}/{banco_de_dados}/jsonrpc. Não há necessidade de projetar estruturas de URL complexas.
  • Chamadas autocontidas: Cada requisição é um objeto JSON completo (método + parâmetros + id), fácil de gerar e processar para LLMs.
  • Tratamento de erros padronizado: Códigos e mensagens de erro fazem parte da especificação.
  • Loteamento (Batching): O protocolo suporta nativamente requisições em lote em uma única viagem de ida e volta HTTP.
  • Descoberta (Discovery): O endpoint de capacidades fornece uma descrição completa da API para agentes de IA sem alucinações.

Por que não REST:

  • Complexidade para IA: A semântica REST está dispersa em vários lugares, dificultando a construção de chamadas confiáveis para agentes de IA.
  • Exposição do esquema: O CRUD sobre tabelas muitas vezes expõe a estrutura interna. O PgArachne expõe funções deliberadamente.
  • Ausência de padrões: O REST não oferece padrão universal para lotes, envelopes de erro ou descoberta automatizada.

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

Para notificações em tempo real, o PgArachne implementa Server-Sent Events (SSE).

Por que SSE:

  • HTTP puro: O SSE é HTTP padrão, funcionando através de proxies e CDNs sem configurações especiais.
  • Suporte nativo do navegador: A API EventSource gerencia a reconexão automática sem bibliotecas externas.
  • Corresponde à semântica do NOTIFY: O NOTIFY do PostgreSQL é unidirecional, adequando-se perfeitamente ao SSE.
  • Multiplexação: Sobre HTTP/2, centenas de fluxos SSE compartilham uma única conexão TCP.
  • Simplicidade operacional: As conexões SSE aparecem como requisições HTTP normais nos logs.

Por que não WebSockets:

  • Bidirecionalidade desnecessária: O cliente nunca precisa enviar dados pelo canal de notificação.
  • Problemas de conectividade: Frequentemente bloqueados por firewalls corporativos e alguns balanceadores de carga em nuvem.
  • Maior sobrecarga: Handshakes e frames de ping/pong desnecessários para o simples streaming de eventos.

6. Estrutura de URL: /{prefixo}/{banco_de_dados}/{endpoint}

O PgArachne roteia todos os endpoints sob um segmento de prefixo configurável: /db/{banco_de_dados}/jsonrpc, /db/{banco_de_dados}/file, /db/{banco_de_dados}/sse, /db/{banco_de_dados}/mcp. O prefixo padrão é db e pode ser alterado via API_PREFIX.

Por que esta estrutura:

  • Roteamento por proxy reverso: Uma única instância do PgArachne pode servir múltiplos bancos de dados. Um proxy reverso pode rotear por prefixo ou nome do banco de dados sem inspecionar o corpo da requisição, o que é essencial para balanceamento de carga.
  • Escalabilidade horizontal: Com o nome do banco de dados no caminho da URL, é possível executar múltiplas instâncias do PgArachne e direcionar o tráfego por banco de dados usando regras de proxy padrão, sem sessões persistentes.
  • Multiplexação de protocolos por banco de dados: Agrupar /jsonrpc, /file, /sse e /mcp sob o mesmo namespace /{prefixo}/{banco_de_dados}/ permite aplicar autenticação, rate limiting e controle de acesso por banco de dados no nível do proxy.
  • Prefixo configurável: Deployments que já usam /api/ podem configurar API_PREFIX=api.
  • Observabilidade: Sistemas de log e métricas podem agrupar o tráfego por nome de banco de dados diretamente da URL sem analisar corpos JSON.

Por que não uma estrutura plana como /api/{banco_de_dados}:

  • Ambiguidade de protocolo: Um único endpoint plano não consegue distinguir o tráfego JSON-RPC, SSE e MCP no nível de roteamento.
  • Mais difícil de estender: Adicionar novos protocolos exigiria de qualquer forma novas rotas; portanto, o namespace estruturado prepara o design para o futuro.

7. MCP como Camada de Tradução, não como Protocolo de Banco de Dados

O PgArachne implementa o Model Context Protocol (MCP) como uma fina camada de tradução no servidor Go. As funções PostgreSQL nunca têm conhecimento do MCP — permanecem simples funções jsonb → json.

Por que traduzir MCP no servidor:

  • Sem alterações nas funções existentes: Qualquer função já exposta via JSON-RPC fica instantaneamente disponível como tool MCP. Sem alterações SQL.
  • MCP é mais do que apenas tools: O protocolo inclui um método de descoberta (server/discover), metadados de versão do protocolo e de capacidades em cada requisição, notificações e extensões (resources, prompts) além de simples chamadas de tools — preocupações de nível de protocolo que pertencem ao Go.
  • A segurança permanece em um único lugar: Autenticação, troca de papel e validação já estão implementados em Go. O endpoint MCP reutiliza essa lógica sem alterações.
  • Múltiplos protocolos, um backend: A mesma função PostgreSQL pode ser chamada via JSON-RPC, MCP ou SSE. O banco de dados é agnóstico ao protocolo.
  • SQL mais simples: Processar envelopes MCP (server/discover, tools/list, tratamento de notificações) em PostgreSQL exigiria analisar estruturas JSON complexas em PL/pgSQL, tornando as funções mais difíceis de escrever, testar e manter.

Por que não levar MCP ao banco de dados:

  • A validação de transporte MCP não requer o banco de dados: server/discover e as verificações de versão de protocolo/cabeçalhos em cada requisição são mensagens de protocolo puras. Abrir uma conexão para elas desperdiça recursos.
  • SQL é a ferramenta inadequada para lógica de protocolo: Códigos de erro JSON-RPC, roteamento de notificações e gerenciamento de chaves de idempotência são preocupações de middleware, não de dados.

8. Exportação de OpenAPI: Caminhos Virtuais, Não um Novo Protocolo

GET /{prefix}/{database}/openapi.json (ou .yaml) gera um documento OpenAPI 3.1 descrevendo todos os métodos que o chamador autenticado pode executar. A invocação real ainda acontece exclusivamente através do endpoint único POST /{prefix}/{database}/jsonrpc — a especificação lista adicionalmente cada método sob seu próprio caminho, como /{prefix}/{database}/rpc/api.hello_world, mas esse caminho é apenas documentação e não existe como uma rota HTTP que você possa chamar diretamente.

Por que caminhos virtuais por método:

  • Compatibilidade com ferramentas: Swagger UI, Postman, Insomnia e a maioria dos geradores de código OpenAPI esperam uma operação por entrada em paths. Um único caminho JSON-RPC não consegue, de outra forma, representar N assinaturas de métodos diferentes de uma forma que essas ferramentas entendam.
  • Nenhuma alteração de backend necessária: Adicionar uma rota real por método significaria uma segunda forma de invocar cada função, com sua própria superfície de autenticação, tratamento de erros e versionamento a ser mantida em sincronia com o JSON-RPC. Gerar os caminhos puramente a partir de capabilities() mantém a superfície do protocolo exatamente como descrita na decisão 4, ao mesmo tempo que satisfaz as ferramentas que precisam de esquemas por operação.
  • Autodocumentado: A descrição de cada operação virtual detalha exatamente a requisição JSON-RPC (método + parâmetros) necessária para realmente chamá-la, portanto nada se perde por não haver uma rota real.

Por que a especificação é autenticada e filtrada por papel:

  • Consistência com o restante da API: Todos os demais endpoints (JSON-RPC, tools/list do MCP) só revelam os métodos que o papel do chamador pode executar. Um documento OpenAPI não autenticado e não filtrado vazaria a existência e o formato dos parâmetros de funções que um determinado chamador não pode realmente invocar.
  • Mesmo mecanismo utilizado em todo o resto: O handler Go autentica com a mesma lógica Basic/JWT/token de API usada em /jsonrpc, e então executa SET LOCAL ROLE antes de gerar a especificação — pgarachne.generate_openapi_spec() é SECURITY INVOKER especificamente para que sua chamada interna a capabilities() veja esse papel, da mesma forma que o tools/list do MCP já funciona.