Decyzje architektoniczne
Decyzje projektowe dotyczące architektury
Ta strona wyjaśnia motywy stojące za kluczowymi wyborami architektonicznymi i technologicznymi w PgArachne. Te decyzje zostały podjęte z priorytetem wydajności, bezpieczeństwa i produktywności programistów, jednocześnie zapewniając wysoką kompatybilność z nowoczesnymi agentami AI i modelami LLM.
1. Czemu PostgreSQL
PgArachne celowo jest zbudowane wyłącznie na PostgreSQL i nie próbuje być niezależne od bazy danych. Większość tego, co oferuje bramka, to nie własna logika w Go, lecz bezpośrednie wykorzystanie możliwości PostgreSQL.
Czemu PostgreSQL:
- Wbudowany model uprawnień: Role,
GRANT/REVOKEi uprawnieniaEXECUTEna funkcjach są częścią bazy danych. PgArachne nie potrzebuje więc własnej warstwy autoryzacji — jedynie przełącza rolę za pomocąSET LOCAL ROLE, a resztą zajmuje się PostgreSQL. Reguły dostępu obowiązują tak samo dla API,psqli każdego innego narzędzia. - Row-Level Security: Polityki na poziomie wierszy są oceniane względem roli, w imieniu której odbywa się wywołanie. Izolacja danych między użytkownikami czy tenantami znajduje się więc w bazie danych, a nie w kodzie bramki.
- Natywny JSON (
jsonb): Kontraktfunkcja(jsonb) → jsondokładnie odpowiada ciału żądania i odpowiedzi JSON-RPC. Nie trzeba mapować parametrów na typy ani generować opakowań — JSON przechodzi przez bramkę bez zmian. - Gwarancje transakcyjne: Każde wywołanie działa w jednej transakcji z pełnymi gwarancjami ACID. Funkcja może atomowo zapisać dane do wielu tabel, a w razie błędu wszystko zostaje wycofane, bez udziału bramki.
- Potężny model programistyczny: PL/pgSQL, funkcje SQL i inne języki proceduralne pozwalają zapisać logikę biznesową tam, gdzie są dane, bez przesyłania wyników pośrednich przez sieć.
LISTEN/NOTIFY: Powiadomienia w czasie rzeczywistym (decyzja 5) opierają się wprost na mechanizmie PostgreSQL. Zewnętrzny broker wiadomości nie jest potrzebny.- Introspekcja katalogu systemowego: Lista wywoływalnych metod, ich opisy i schematy parametrów są odczytywane z
pg_proci komentarzy funkcji. Z jednego źródła prawdy powstającapabilities, MCPtools/listi specyfikacja OpenAPI, więc nie mogą rozjechać się z rzeczywistością. - Ekosystem rozszerzeń: PostGIS, pgvector, TimescaleDB,
pg_trgmi inne rozszerzenia są od razu dostępne jako zwykłe funkcje SQL, a tym samym jako metody API i narzędzia dla agentów AI — bez ani jednej linijki kodu w Go. - Otwartość i dojrzałość: Wolna licencja bez vendor lock-in, wieloletnia sprawdzona praktyka produkcyjna, aktywny rozwój i dostępność u wszystkich znaczących dostawców chmurowych, a także jako samodzielnie utrzymywana instancja.
Czemu nie inna baza danych ani warstwa niezależna od bazy:
- Najmniejszy wspólny mianownik: Obsługa wielu baz danych oznaczałaby rezygnację właśnie z tych cech, na których opiera się PgArachne — modelu bezpieczeństwa opartego na rolach,
jsonb,LISTEN/NOTIFYi introspekcji funkcji. Pozostałaby cienka i mniej bezpieczna warstwa nad generycznym SQL. - MySQL/MariaDB: Nie mają natywnego Row-Level Security ani odpowiednika
LISTEN/NOTIFY, a ich wsparcie dla JSON i logiki proceduralnej jest bardziej ograniczone. - SQL Server i Oracle: Własnościowe licencje i koszty operacyjne kłócą się z celem łatwego, bezpłatnego wdrożenia jednym plikiem binarnym.
- Bazy NoSQL (dokumentowe, klucz-wartość, kolumnowe): Skalowanie i elastyczność schematu, dla których wybiera się NoSQL, nie są dla PgArachne głównym atutem. Potrzebny magazyn dokumentów zapewnia
jsonbwraz z indeksowaniem i zapytaniami, obok danych relacyjnych i w tej samej transakcji. Brakuje za to tego, na czym stoi PgArachne: funkcji po stronie serwera wywoływanych z granularnym uprawnieniemEXECUTEdla konkretnej roli, Row-Level Security, introspekcji funkcji z katalogu i jednolitego języka zapytań. Bez nich bramka musiałaby sama obsługiwać autoryzację, walidację i opis API — i przestałaby być cienką warstwą. - SQLite: To baza wbudowana, bez serwerowego modelu ról i uprawnień, na którym opiera się całe zabezpieczenie PgArachne.
W rezultacie PgArachne pozostaje małe: bezpieczeństwo, transakcje, powiadomienia i odkrywanie API deleguje do PostgreSQL, a samo zajmuje się wyłącznie translacją protokołów i uwierzytelnianiem.
2. Funkcje PostgreSQL jako powierzchnia API
PgArachne celowo udostępnia funkcje bazy danych, a nie surowe tabele.
Czemu funkcje:
- Enkapsulacja: Logika biznesowa żyje razem z danymi w bazie — jedno miejsce do audytu, wersjonowania i zabezpieczenia.
- Wyraźne bezpieczeństwo: Przez API dostępne są tylko funkcje, którym jawnie przyznano uprawnienia
EXECUTEdla konkretnej roli. - Abstrakcja: Walidacja danych wejściowych, obliczone pola i złożone operacje na wielu tabelach są skryte przed klientem, co zapewnia czysty interfejs.
Czemu nie CRUD na poziomie tabel:
- Ścisłe powiązanie: Udostępnianie tabel bezpośrednio wiąże Twoje API z wewnętrznym schematem bazy danych, co utrudnia refaktoryzację bazy bez psucia klientów.
- Fragmentacja reguł biznesowych: Logika biznesowa kończy podzielona między ograniczenia bazy danych i to, jaki middleware jest używany do filtrowania żądań HTTP.
3. Go vs. alternatywy
PgArachne jest napisane w Go, aby zapewnić najlepszy balans między wydajnością a prostotą wdrożenia.
Czemu Go:
- Statyczne binarki: Kompiluje się do jednego pliku binarnego bez zewnętrznych zależności. Wdrożenie jest tak proste jak skopiowanie pliku na serwer.
- Współbieżność: Goroutines w Go pozwalają obsługiwać tysiące współbieżnych połączeń SSE i bazodanowych w sposób lekki i bezpośredni.
- Solidna biblioteka standardowa: Wbudowane biblioteki do HTTP, TLS i JSON są na poziomie produkcyjnym i nie wymagają żadnych „node_modules” ani zewnętrznych runtime’ów.
- Kompilacja krzyżowa: Łatwo celuje w Linux, macOS i Windows (amd64 i arm64) z każdej maszyny programistycznej.
Czemu nie Node.js, PHP czy Ruby:
- Runtime’y: Te technologie wymagają instalacji konkretnego środowiska uruchomieniowego na każdej maszynie docelowej.
- Efektywność: Jednowątkowa pętla Node.js lub model process-per-request w PHP są mniej efektywne w utrzymywaniu tysięcy bezczynnych połączeń SSE.
- Zużycie pamięci: Go zużywa znacznie mniej pamięci na połączenie niż języki skryptowe.
Czemu nie Rust:
- Tempo rozwoju: Choć Rust zapewnia ekstremalną wydajność, jego złożoność (borrow checker) zwalnia iterację dla narzędzia zdominowanego przez operacje I/O, gdzie wydajność Go jest już więcej niż wystarczająca.
Czemu nie C/C++:
- Bezpieczeństwo: Ręczne zarządzanie pamięcią wprowadza znaczące ryzyka bezpieczeństwa (przepełnienia bufora) bez realnego zysku wydajności w aplikacji typu bramka (gateway).
4. JSON-RPC 2.0 vs. REST
PgArachne używa JSON-RPC 2.0 jako podstawowego protokołu komunikacji zamiast tradycyjnego REST.
Czemu JSON-RPC 2.0:
- Jeden endpoint: Cała komunikacja odbywa się przez
POST /{prefix}/{database}/jsonrpc. Nie trzeba projektować złożonych struktur URL ani debatować o semantyce metod HTTP. - Samodzielne wywołania: Każde żądanie to kompletny obiekt JSON (metoda + parametry + id). Ten format jest trywialny do wygenerowania i sparsowania przez LLM i agentów AI z wysoką wiarygodnością.
- Standaryzowana obsługa błędów: Kody błędów i komunikaty są częścią specyfikacji, co eliminuje potrzebę „wymyślania” konwencji kodów statusu HTTP dla błędów biznesowych.
- Batchowanie: Protokół natywnie wspiera żądania zbiorcze (batch), umożliwiając wykonanie wielu operacji (np. kilku wywołań funkcji) w jednym cyklu żądanie-odpowiedź HTTP bez dodatkowej pracy.
- Odkrywanie (discovery): Endpoint capabilities dostarcza pełny opis API w formacie, który agenci AI mogą wykorzystać, by zrozumieć dostępne narzędzia bez halucynacji.
Czemu nie REST:
- Złożoność dla AI: Semantyka REST (GET/POST/PATCH/DELETE + parametry URL + body) jest rozproszona w wielu miejscach, co utrudnia agentom AI wiarygodne konstruowanie wywołań.
- Wyciek schematu: CRUD-nad-tabelami (jak w PostgREST) często ujawnia wewnętrzną strukturę bazy danych bezpośrednio w API. PgArachne celowo udostępnia funkcje, zachowując logikę biznesową zamkniętą w SQL.
- Brak standardów: REST nie oferuje żadnego uniwersalnego standardu dla operacji zbiorczych, uniwersalnych struktur błędów czy automatycznego odkrywania API.
5. SSE (Server-Sent Events) vs. WebSockets
Do powiadomień w czasie rzeczywistym PgArachne implementuje Server-Sent Events (SSE).
Czemu SSE:
- Zwykłe HTTP: SSE to standardowe HTTP. Działa przez proxy, load balancery i CDN bez specjalnej konfiguracji „protocol upgrade”.
- Natywne wsparcie w przeglądarce: API
EventSourcejest wbudowane we wszystkie nowoczesne przeglądarki i obsługuje automatyczne ponowne łączenie bez żadnych bibliotek klienckich. - Odpowiada semantyce NOTIFY: PostgreSQL
NOTIFYjest jednokierunkowy (serwer do klienta), co idealnie odpowiada modelowi SSE. - Multipleksowanie: Przez HTTP/2 setki strumieni SSE mogą współdzielić jedno połączenie TCP, co jest wyjątkowo efektywne.
- Prostota operacyjna: Połączenia SSE wyglądają jak normalne żądania HTTP w logach i narzędziach monitorujących, co ułatwia debugowanie i ograniczanie ruchu (rate limiting).
Czemu nie WebSockets:
- Niepotrzebna dwukierunkowość: Skoro klient nigdy nie musi wysyłać danych z powrotem przez kanał powiadomień, złożoność WebSocketów nie daje żadnej korzyści.
- Problemy z łącznością: WebSockety są często blokowane lub przedwcześnie zamykane przez firewalle korporacyjne i niektóre chmurowe load balancery.
- Wyższy narzut: Dodaje złożoność protokołu (handshake, ramki ping/pong), która nie jest wymagana do prostego strumieniowania zdarzeń.
6. Struktura URL: /{prefix}/{database}/{endpoint}
PgArachne kieruje wszystkie endpointy pod konfigurowalny segment prefiksu:
/db/{database}/jsonrpc, /db/{database}/file, /db/{database}/sse, /db/{database}/mcp.
Prefiks domyślnie ma wartość db i można go zmienić za pomocą API_PREFIX.
Czemu taka struktura:
- Routing przez reverse proxy: Jedna instancja PgArachne może obsługiwać wiele baz danych. Reverse proxy (Nginx, Caddy, Traefik) może kierować ruch na podstawie prefiksu lub nazwy bazy danych bez inspekcji ciała żądania, co jest kluczowe dla load balancingu i reguł routingu bazujących na ścieżce.
- Skalowalność horyzontalna: Mając nazwę bazy danych w ścieżce URL, można uruchomić wiele instancji PgArachne i kierować ruch do konkretnych instancji na podstawie bazy danych za pomocą standardowych reguł proxy — bez sticky sessions czy inspekcji ciała żądania.
- Multipleksowanie protokołów per baza danych: Grupowanie
/jsonrpc,/file,/ssei/mcppod tą samą przestrzenią nazw/{prefix}/{database}/naturalnie umożliwia zastosowanie autoryzacji, rate limitingu i kontroli dostępu per baza danych na poziomie proxy. - Konfigurowalny prefiks: Wdrożenia, które już używają
/api/jako prefiksu w swojej infrastrukturze, mogą ustawićAPI_PREFIX=api, aby dopasować się do swoich konwencji. - Obserwowalność: Agregatory logów i systemy metryk mogą grupować i filtrować ruch po nazwie bazy danych bezpośrednio z URL bez parsowania ciał JSON.
Czemu nie płaska struktura jak /api/{database}:
- Niejednoznaczność protokołu: Jeden płaski endpoint nie może rozróżnić ruchu JSON-RPC, SSE i MCP na poziomie routingu — ta decyzja spada na logikę aplikacji lub inspekcję nagłówków.
- Trudniejsze rozszerzanie: Dodanie nowych protokołów (np. GraphQL, gRPC-gateway) i tak wymaga wprowadzenia nowych ścieżek najwyższego poziomu, więc strukturyzowana przestrzeń nazw zabezpiecza projekt na przyszłość.
7. MCP jako warstwa translacji, nie protokół bazy danych
PgArachne implementuje Model Context Protocol (MCP)
jako lekką warstwę translacji w serwerze Go. Funkcje PostgreSQL nigdy nie są świadome MCP — pozostają
prostymi funkcjami jsonb → json.
Czemu tłumaczyć MCP na serwerze:
- Zero zmian w istniejących funkcjach: Każda funkcja już udostępniona przez JSON-RPC jest natychmiast dostępna jako narzędzie MCP. Żadnych zmian w SQL, żadnego ponownego wdrażania obiektów bazy danych.
- MCP to więcej niż tylko narzędzia: Protokół obejmuje metodę wykrywania (
server/discover), metadane wersji protokołu i możliwości przesyłane z każdym żądaniem, powiadomienia oraz rozszerzenia (zasoby, prompty) wykraczające poza proste wywołania narzędzi. To sprawy na poziomie protokołu, które należą do Go, nie do funkcji SQL. - Bezpieczeństwo pozostaje w jednym miejscu: Autoryzacja, przełączanie ról i walidacja danych wejściowych są już zaimplementowane w Go. Endpoint MCP wykorzystuje tę logikę bez zmian.
- Wiele protokołów, jeden backend: Ta sama funkcja PostgreSQL może być wywoływana przez JSON-RPC (ze zwykłego klienta), MCP (z Claude Desktop lub Cursor) lub SSE (dla subskrypcji zdarzeń). Baza danych jest niezależna od protokołu.
- Prostszy SQL: Przetwarzanie obwiedni MCP (
server/discover,tools/list, obsługa powiadomień) wewnątrz funkcji PostgreSQL wymagałoby parsowania złożonych struktur JSON w PL/pgSQL, co uczyniłoby funkcje trudniejszymi do pisania, testowania i utrzymania.
Czemu nie przenieść MCP do bazy danych:
- Walidacja transportu MCP nie potrzebuje bazy danych:
server/discoveroraz sprawdzanie wersji protokołu i nagłówków przy każdym żądaniu to czyste komunikaty protokołu. Otwieranie połączenia z bazą danych dla nich marnuje zasoby i dodaje latencję. - SQL to niewłaściwe narzędzie do logiki protokołu: Kody błędów JSON-RPC 2.0, routing powiadomień i zarządzanie kluczami idempotencji to zagadnienia middleware, nie zagadnienia danych.
8. Eksport OpenAPI: wirtualne ścieżki, nie nowy protokół
GET /{prefix}/{database}/openapi.json (lub .yaml) generuje dokument OpenAPI 3.1
opisujący każdą metodę, którą uwierzytelniony wywołujący może wykonać. Rzeczywiste wywołanie nadal odbywa się
wyłącznie przez jeden endpoint POST /{prefix}/{database}/jsonrpc — specyfikacja dodatkowo wymienia
każdą metodę pod własną ścieżką, taką jak /{prefix}/{database}/rpc/api.hello_world, ale ta ścieżka
służy wyłącznie dokumentacji i nie istnieje jako trasa HTTP, którą można wywołać bezpośrednio.
Czemu wirtualne ścieżki per metoda:
- Kompatybilność z narzędziami: Swagger UI, Postman, Insomnia i większość generatorów kodu OpenAPI oczekują jednej operacji na wpis w
paths. Pojedyncza ścieżka JSON-RPC nie może inaczej reprezentować N różnych sygnatur metod w sposób zrozumiały dla tych narzędzi. - Brak zmian w backendzie: Dodanie prawdziwej trasy dla każdej metody oznaczałoby drugi sposób wywoływania każdej funkcji, z własną warstwą autoryzacji, obsługi błędów i wersjonowania, którą trzeba by utrzymywać w zgodzie z JSON-RPC. Generowanie ścieżek wyłącznie na podstawie
capabilities()zachowuje powierzchnię protokołu dokładnie taką, jak opisano w decyzji 4, jednocześnie spełniając wymagania narzędzi potrzebujących schematów per operacja. - Samodokumentowanie: Opis każdej wirtualnej operacji dokładnie wyjaśnia żądanie JSON-RPC (metodę + parametry) potrzebne do jej faktycznego wywołania, więc brak prawdziwej trasy niczego nie traci.
Czemu specyfikacja jest uwierzytelniona i filtrowana według ról:
- Spójność z resztą API: Każdy inny endpoint (JSON-RPC, MCP
tools/list) ujawnia wyłącznie metody, które rola wywołującego może wykonać. Nieuwierzytelniony i niefiltrowany dokument OpenAPI ujawniałby istnienie i kształt parametrów funkcji, których dany wywołujący w rzeczywistości nie może wywołać. - Ten sam mechanizm co wszędzie indziej: Handler w Go uwierzytelnia za pomocą tej samej logiki Basic/JWT/token API co
/jsonrpc, a następnie wykonujeSET LOCAL ROLEprzed wygenerowaniem specyfikacji —pgarachne.generate_openapi_spec()ma atrybutSECURITY INVOKERwłaśnie po to, by jej wewnętrzne wywołaniecapabilities()widziało tę rolę, dokładnie tak jak już działa MCPtools/list.