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. 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.
2. 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ń.
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. 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.
5. Struktura URL: /{prefix}/{database}/{endpoint}
PgArachne kieruje wszystkie endpointy pod konfigurowalny segment prefiksu:
/db/{database}/jsonrpc, /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,/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ść.
6. 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 uzgadnianie inicjalizacyjne (handshake), ping, powiadomienia i potencjalne przyszłe rozszerzenia (zasoby, prompty, sampling). 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 (
initialize,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:
- Handshake MCP nie potrzebuje bazy danych:
initializeipingto 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.