pgarachne
AGENTS.md7 starsChanged 35 days agoMITView on GitHub
- Reads credentials
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository2 other files it gives its agents.
- docs/llms-full.txtllms.txt
- docs/llms.txtllms.txt
The file
We split this AGENTS.md into the rules it gives an agent
11 rules. About 0 are common, 4 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 31
All endpoints (JSON-RPC, MCP, SSE, OpenAPI) authenticate through authenticateForDatabase in server.go — don't add a parallel auth path, or …
- Very specific: line 138
All of the above is validated centrally in mcpValidateRequest (internal/server/mcp.go) before method dispatch.
- Very specific: line 157
Every result carries resultType: "complete" (PgArachne never needs mid-call client input, so this is the only value it emits) and …
- Very specific: line 179
All endpoints follow /{prefix}/{database}/{protocol} (e.g.
- Very specific: line 226
Test helpers: mcpDo, decodeMCPResponse, newProtocolTestServer, insertTestPrompt, deleteTestPrompt.
- Very specific: line 229
Keep solutions aligned with the JSON-RPC + PostgreSQL function model (single jsonb param).
- Distinctive: line 230
Do not suggest bypassing DB role security.
- Very specific: line 231
When adding features, consider updates needed in both Go server and SQL files (schema.sql and/or mcp_functions.sql).
- Distinctive: line 233
Prefer changes that preserve the minimal middleware footprint and performance.
- Distinctive: line 234
Keep release artifacts and docs in sync when changing file names/URLs.
- Distinctive: line 242
Do not wait for a special request when the update is clearly relevant.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
PgArachne AI Context
Project Summary
PgArachne is a Go-based HTTP gateway that exposes PostgreSQL functions as JSON-RPC 2.0 endpoints. It maps the JSON-RPC method to schema.function(jsonb) calls, impersonates a DB role via SET LOCAL ROLE, and returns JSON-RPC responses. It also provides a capabilities method backed by SQL introspection in sql/schema.sql, an SSE endpoint that forwards PostgreSQL NOTIFY payloads to clients, and a native MCP (Model Context Protocol) endpoint that allows AI clients to discover and call PostgreSQL functions as tools.
Technology Stack
- Language: Go (module in
go.mod) - HTTP framework: Gin (
github.com/gin-gonic/gin) - DB driver:
lib/pq - Auth: JWT (
github.com/golang-jwt/jwt/v4) + DB-backed API tokens - JWT issue/parse helpers live in
internal/auth/jwt.go; SHA-256 token hashing for long-lived API tokens lives ininternal/auth/auth.go - Observability: Prometheus metrics (
/metrics) - Release/build: GoReleaser + Makefile orchestration
- Docs: Hugo sources in
docs-src/, generated static site indocs/; i18n strings indocs-src/i18n/*.yaml - Docs publishing: GitHub Pages serves
docs/directly.docs-src/static/.nojekyllmust stay — without it Jekyll drops dot-directories and/.well-known/security.txt404s. llms.txtis hand-curated indocs-src/static/;llms-full.txtis generated by Hugo (docs-src/layouts/index.llmsfull.txt, output formatllmsfullenabled only incontent/en/_index.htmlfront matter) and moved fromdocs/en/todocs/bymake docs.
Runtime Flow (Core)
cmd/pgarachne/main.goloads config, configures logging, then runs the server.internal/server/server.goregisters routes and runs the HTTP server.- Requests to
/{prefix}/:database/jsonrpc:- JSON-RPC request body is parsed and
methodis required. - DB role is determined via JWT or API token middleware.
- A transaction is opened,
SET LOCAL ROLEis applied, andschema.function(jsonb)is called. - The role switch is issued as
SELECT set_config('role', $1, true)(the function form ofSET LOCAL), not as concatenated SQL text — keep it that way, the role name must stay a bind parameter. capabilitiesis handled specially and maps topgarachne.capabilities.
- JSON-RPC request body is parsed and
Authentication
- Login endpoint:
POST /{prefix}/:database/token(handleToken) takes HTTP Basic credentials (never JSON params), verifies them with a one-off DB connection (no cached pool) and returns{token, token_type: "Bearer", expires_in}withCache-Control: no-store; the JWT carriesdb_roleanddb_nameclaims. Only available whenJWT_SECRETis set. It deliberately sends noWWW-Authenticateheader (it would trigger the browser's login dialog). The old JSON-RPC methodget_jwtwas removed (breaking) —/jsonrpcanswers it with 404 /-32601pointing to/token. A dedicated path (not a JSON-RPC method) lets reverse proxies rate-limit logins without reading bodies. - Direct credentials:
Authorization: Basic …on any endpoint opens a per-user pool authenticated as that user (noSET LOCAL ROLE). - All endpoints (JSON-RPC, MCP, SSE, OpenAPI) authenticate through
authenticateForDatabaseinserver.go— don't add a parallel auth path, or it will miss rate limiting. A request without anAuthorizationheader is rejected before any DB connection is opened. - Protected calls:
POST /{prefix}/:database/jsonrpcaccepts:Authorization: Bearer <jwt>(validated and scoped to the database)- Or a long-lived API token validated by
SELECT pgarachne.verify_api_token($1) - Or
Authorization: Basicdirect credentials
- Auth is identical for the MCP endpoint —
initializeandpingare unauthenticated; all other MCP methods require a valid token.
Database Schema
sql/schema.sql
pgarachne.api_tokens— stores hashed long-lived tokens.pgarachne.add_api_token/pgarachne.verify_api_token— mint and verify tokens.pgarachne.capabilities(jsonb)— introspectspg_procto list callable functions; used by both JSON-RPC and MCPtools/list.pgarachne.generate_openapi_spec(server_url_base, db_name)— builds an OpenAPI 3.1 document for the current database, filtered to the methods the calling role may execute. Pulls the method list frompgarachne.capabilities()so the spec stays in sync.pathscontains the real/jsonrpcoperation (with its per-method metadata still also carried in thex-pgarachne-methodsextension, for tooling that understands it) plus one virtual, documentation-only/rpc/{method}path per exposed method, for tooling that expects one path per operation (Swagger UI, Postman, codegen). The function is deliberatelySECURITY INVOKER(notDEFINER) so that its internal call tocapabilities()— which filters viahas_function_privilege(current_user, ...)— sees the caller's actual role.GRANT EXECUTE ... TO publicis set.pgarachne.requests+pgarachne.save_idempotency_key(key text, scope text)— idempotency key deduplication (shared by JSON-RPC and MCP). Keys are namespaced per role (scope= authenticated role from Go, orcurrent_userfor direct auth) so one caller cannot reserve another's key; the old unscopedsave_idempotency_key(text)is dropped byschema.sql.pgarachne.capabilities()only lists functions whose single argument isjsonb(the gateway callsfn($1::jsonb)); a malformed--- PARAMS ---JSON in a function comment falls back to the default schema instead of failing.- No schema allowlist: access is governed by PostgreSQL privileges alone.
capabilities()lists every non-procedure function with a singlejsonbargument on which the caller hasEXECUTEandUSAGEon its schema, hiding (not blocking)pg_*/information_schema/pgarachne(exceptcapabilities) and extension-owned functions.pgarachne.allowed_schemas()was removed (it only filtered the listing; the gateway never enforced it) — don't reintroduce a listing-only filter. Because PostgreSQL grantsEXECUTEon new functions andUSAGEon schemapublictoPUBLIC, docs recommendALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLICand a dedicated API schema.
sql/mcp_functions.sql
Additional SQL objects required by the MCP resources/* and prompts/* methods:
pgarachne.prompts— table storing named prompt templates (name,description,template,arguments).GRANT SELECT TO publicallows authenticated roles to read.pgarachne.mcp_list_resources(jsonb)— lists all tables/views the current role may SELECT from, as MCP resource descriptors (db:///schema/tableURIs).pgarachne.mcp_read_resource(jsonb)— reads up to 100 rows from a table/view identified by adb:///schema/tableURI; validates existence and privilege before executing the dynamic query.pgarachne.mcp_list_prompts(jsonb)— returns all rows frompgarachne.promptsas MCP prompt descriptors.pgarachne.mcp_get_prompt(jsonb)— returns a rendered prompt template with{{variable}}placeholders substituted fromparams.arguments.
sql/seed_data.sql
Optional example function api.server_info(params jsonb) returning PostgreSQL server
details (server_version, current_user, current_database, current_time).
Loaded by scripts/setup_test_db.sh so integration tests can exercise at least one
public-schema function beyond api.hello_world. Load manually for demos:
psql -d mydb -f sql/seed_data.sql.
sql/universal_table_access.sql
Optional PostgREST-like CRUD helpers: api.universal_read, api.universal_create,
api.universal_update, api.universal_delete — each accepts a single jsonb
parameter and dispatches to any schema.table the role has privileges on.
universal_read validates each comma-separated select item as * or a plain
column name and re-emits it with quote_ident; order is validated against a
column-list/ASC/DESC whitelist. universal_update/universal_delete refuse an
empty filters object unless "all": true is passed. Treat these as convenience helpers; review the SQL before exposing
them on a public endpoint.
sql/users.sql
Optional demo seed: creates the pgarachne service role (if missing), a demo
role with password Demo1234, grants demo to pgarachne, and mints a single
API token for the demo role. Not loaded by the test setup — only meant
for manual / demo installs.
Configuration
Loaded from pgarachne.env (current dir), $XDG_CONFIG_HOME/pgarachne/pgarachne.env, or /etc/pgarachne/pgarachne.env, or directly via environment variables.
Required:
DB_HOST,DB_PORT,DB_USER
Optional JWT:
JWT_SECRET(minimum 32 bytes, e.g.openssl rand -hex 32). When unset, JWT is disabled:/tokenreturns HTTP 404 /-32601, Bearer values are only checked as API tokens, and clients use HTTP Basic credentials or API tokens.Server.jwtEnabled()is the single switch.
Common optional:
HTTP_PORT(default8080)API_PREFIX(defaultdb) — first URL path segment for all database endpoints; gives routes like/db/:database/jsonrpc. Only letters, digits, hyphens and underscores allowed.JWT_EXPIRY_HOURS(default8, must be > 0)ALLOWED_ORIGINS(comma-separated; unset = cross-origin browser requests disabled, set*explicitly to allow any origin)STATIC_FILES_PATH(serves files viaNoRoutefallback; the directory is pinned withos.OpenRootat startup and every request is resolved through that*os.Root, so traversal and symlink escapes are refused by the OS — do not reintroducefilepath.Join+ string containment checks here)LOG_LEVEL(INFOdefault),LOG_OUTPUT(stdoutdefault)DB_SSLMODE(defaultrequire; setdisableexplicitly for local non-TLS PostgreSQL)LOGIN_RATE_LIMIT(default5) /LOGIN_RATE_LIMIT_PER_IP(default 5×LOGIN_RATE_LIMIT,0disables) /LOGIN_RATE_WINDOW(default1m) — apply to all auth methods on all endpoints:/tokencounts every attempt (loginLimiter.Allow); Basic auth counts failed attempts per (IP, username) and per IP, failed Bearer (JWT/API token) attempts per IP (Blocked/RecordFailure, centralised inauthenticateForDatabase). Exhausted budget → HTTP 429 before credentials are checked.MCP_SQL_ERROR_DETAIL(defaultfalse;trueincludes raw PostgreSQL error messages in MCP tool errors — helps LLM agents self-correct, but leaks schema details to authenticated callers)DIRECT_POOL_LIMIT(default1000; max distinct Basic-Auth credential pools)DB_SSLROOTCERT,DB_SSLCERT,DB_SSLKEY(optional TLS cert paths)FILE_MAX_BYTES(default 64 MiB) /FILE_MAX_ENTRIES(default 1000) — caps for/fileresponsesMAX_REQUEST_BYTES(default body size limit, protects against oversized request DoS)TRUSTED_PROXIES(recommended when running behind reverse proxy; affects client IP handling)METRICS_ENABLED(defaulttrue)METRICS_LISTEN_ADDR(default127.0.0.1:9090, dedicated local-only metrics listener)PID_FILE(optional daemon PID file path for-start/-stop; default to user cache dir, fallback temp dir)- SSE settings:
SSE_MAX_CHANNELS,SSE_MAX_CLIENTS,SSE_CLIENT_BUFFER,SSE_SEND_TIMEOUT,SSE_HEARTBEAT,SSE_IDLE_TIMEOUT
HTTP Endpoints
GET /health— health checkPOST /{prefix}/:database/jsonrpc— JSON-RPC 2.0 gatewayPOST /{prefix}/:database/token— JWT login with HTTP Basic credentials (see Authentication)POST /{prefix}/:database/file— binary download (internal/server/file.go). CallsSELECT * FROM schema.fn($1::jsonb)on a set-returning function with columnspath,content(required) andmime_type,store_only(optional). 1 row → served directly; ≥2 rows oroptions.force_zip→ ZIP (archive/zip, streamed). SameauthenticateForDatabase+setupRequestTxas JSON-RPC. Rows are buffered (capsFILE_MAX_BYTES/FILE_MAX_ENTRIES); DB never controls headers — alwaysattachment+nosniff+CSP: sandbox;validateEntryPath(zip-slip + Windows-hostile names),claimPath(case-insensitive/file-vs-dir collisions),safeMIMEandsanitizeDownloadNameguard DB-supplied values; an empty result is rolled back (404); onlyRETURNS TABLE/OUT-param set-returning functions are discovered askind: file; errors are JSON (0 rows → 404).capabilities()marks such functionskind: "file"(set-returning withpath+contentOUT columns); they are excluded from the/jsonrpcOpenAPI paths and MCPtools/list. Planned phase 2 (issue #7):GET /{prefix}/:database/get/{token}with dedicated scoped link tokens (hash-stored, bound to role+function+params, read-only) reusing the same response writer.GET /{prefix}/:database/sse— SSE stream for PostgreSQLNOTIFYchannels (channelsquery param required). Authenticates the caller but, unlike every other endpoint here, does notSET LOCAL ROLEor check per-channel permissions — all SSE clients for a database share oneLISTENconnection opened asDB_USER, so any authenticated caller can subscribe to any channel name. This is by design (Postgres channels aren't objects with their own GRANTs) and documented indocs-src/content/en/real-time-notifications.html; don't "fix" it locally without reading that note first.POST /{prefix}/:database/mcp— MCP (Model Context Protocol) Streamable HTTP endpointGET /{prefix}/:database/openapi.json(or/openapi.yaml, or?format=yaml) — OpenAPI 3.1 spec generated on the fly bypgarachne.generate_openapi_spec. Requires the same auth as/jsonrpc(Basic / Bearer JWT / API token); the returned spec is filtered to the methods the authenticated role may execute, same as MCPtools/list. Served asapplication/jsonby default, orapplication/yamlfor the YAML variant.GET /metrics— Prometheus metrics on the dedicated metrics listener (METRICS_LISTEN_ADDR)
MCP Endpoint Detail (/{prefix}/{database}/mcp)
Implements the MCP Streamable HTTP transport, protocol version 2026-07-28. This
revision made the protocol fully stateless: there is no initialize/session
handshake, no Mcp-Session-Id, and no GET/SSE stream — every request is an
independent POST that declares its own protocol version and capabilities.
GET and DELETE on the endpoint return 405.
Transport-level requirements (every request)
params._metamust includeio.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilities→ missing either is-32602(Invalid params), HTTP 400.- HTTP headers
MCP-Protocol-VersionandMcp-Methodare required and must agree with the body's_metaversion andmethod→ disagreement is-32020(HeaderMismatch), HTTP 400. tools/call,resources/read,prompts/getadditionally require anMcp-Nameheader matchingparams.name(orparams.uriforresources/read) → sameHeaderMismatcherror on mismatch.- An unsupported
protocolVersion(anything but2026-07-28) →-32022(UnsupportedProtocolVersionError), HTTP 400,data.supportedlists the versions this server accepts. - All of the above is validated centrally in
mcpValidateRequest(internal/server/mcp.go) before method dispatch.
Method mapping
| MCP method | Auth | Action |
server/discover | No | Returns supported protocol versions, capabilities (tools + resources + prompts), server identity |
notifications/* | No | Returns HTTP 202, no body |
tools/list | Yes | Calls pgarachne.capabilities() as authenticated role; maps to MCP tool descriptors |
tools/call | Yes | Calls schema.function(arguments::jsonb) as authenticated role; wraps result in MCP text content block |
resources/list | Yes | Calls pgarachne.mcp_list_resources() as authenticated role |
resources/read | Yes | Calls pgarachne.mcp_read_resource({uri}) as authenticated role |
prompts/list | Yes | Calls pgarachne.mcp_list_prompts() as authenticated role |
prompts/get | Yes | Calls pgarachne.mcp_get_prompt({name, arguments}) as authenticated role |
initialize and ping no longer exist — both were removed from the core
protocol in 2026-07-28.
Result shape
Every result carries resultType: "complete" (PgArachne never needs
mid-call client input, so this is the only value it emits) and
_meta["io.modelcontextprotocol/serverInfo"]. server/discover,
tools/list, resources/list, resources/read, and prompts/list are also
CacheableResults and additionally carry ttlMs + cacheScope ("private"
for anything gated by the authenticated role's grants, "public" for
server/discover). Built centrally by mcpWrapResult/mcpWrapCacheableResult
in internal/server/mcp.go.
Adding new MCP methods
Register the MCP method name, its SQL backing function, and its cache hints
in the mcpDatabaseMethods map in internal/server/mcp.go. No other Go
changes needed.
Error model
- SQL failures in
tools/call→ tool-level error (isError: truein result content), NOT a JSON-RPC protocol error — per MCP spec. - SQL failures in
resources/*andprompts/*→ JSON-RPC protocol error (errorfield) — these are infrastructure queries, not user tools.
Idempotency
tools/call supports _meta.idempotencyKey in params, identical semantics to the JSON-RPC idempotencyKey field. Both use pgarachne.save_idempotency_key().
URL Structure Rationale
All endpoints follow /{prefix}/{database}/{protocol} (e.g. /db/mydb/jsonrpc, /db/mydb/sse, /db/mydb/mcp). The prefix defaults to db and is configurable via API_PREFIX. This structure enables:
- Reverse proxy routing by prefix/database name without body inspection.
- Horizontal scaling: route traffic to per-database instances via standard proxy rules.
- Per-database auth/rate-limiting at the proxy layer.
- Observability: log aggregators can filter by database from the URL alone.
Key Files
cmd/pgarachne/main.go— CLI entrypoint, logging, config, daemon flagsinternal/server/server.go— routes, auth, JSON-RPC executioninternal/server/mcp.go— MCP Streamable HTTP handler;mcpDatabaseMethodsmap for extensibilityinternal/server/file.go—/fileendpoint: row validation, single-file/ZIP responseinternal/server/sse.go— SSE hub, per-databasepq.Listener, client broadcast, metrics,Shutdown(ctx)for graceful termination. Locking rule:dbListener.mu(maps) is never held across a LISTEN/UNLISTEN round-trip (subMuserialises those), andrun()/ the pq event callback never call into the listener —dropClientonly closesclient.done, the handler goroutine unregisters. Breaking this deadlocks pq's connection loop.internal/database/database.go— connection pool per database,CloseAll()for graceful shutdown. Pools are opened and pinged outside the global mutexes, so a slow server or a wrong-password attempt never stalls other requests.internal/server/metrics.go— Prometheus collectors + HTTP/method-level middlewareinternal/server/types.go— JSON-RPC envelope types (JSONRPCRequest,JSONRPCResponse,LoginRequest)internal/server/mcp_test.go— MCP unit + integration tests (protocol-level tests run without DB; integration tests requirePGARACHNE_TEST_DB=1)internal/server/integration_test.go— JSON-RPC + SSE integration testsinternal/server/validation_test.go— identifier safety validation testsinternal/database/database.go— connection pool per databaseinternal/config/config.go— config loading and validation (includesAPIPrefix)internal/auth/auth.go— token hashing helper (SHA-256)internal/auth/jwt.go— JWT issue/parse primitives (Issue,Parse,Claims,ErrInvalidToken)internal/daemon/*— unix background start/stop, Windows stubssql/schema.sql— core DB schema: tokens, capabilities, idempotency, OpenAPI generationsql/mcp_functions.sql— MCP-specific SQL:pgarachne.promptstable,mcp_list_resources,mcp_read_resource,mcp_list_prompts,mcp_get_promptsql/seed_data.sql— optional example functionapi.server_info; loaded by test setupsql/universal_table_access.sql— optional PostgREST-like CRUD helperssql/users.sql— optional demo roles + initial API tokenscripts/setup_test_db.sh— appliesschema.sql,mcp_functions.sql, andseed_data.sqlto the test databasescripts/run_tests.sh— spins up Docker Postgres, callssetup_test_db.sh, runsgo test ./...tools/explorer/— interactive JSON-RPC explorer (PWA, dark/light theme, offline-capable). Served whenSTATIC_FILES_PATHpoints to this directory; URL is/tools/explorer/(or/tools/api-explorer/per Hugo docs)tools/jwt-signer/— offline JWT Signer: signs HS256 tokens in the browser from a hand-enteredJWT_SECRET(Web Crypto). Must stay network-free (CSPconnect-src 'none', no external assets) and must never persist the secret; its hosted copy isjwt-signer.pgarachne.comtools/sse-tester/— minimal HTML page for manually exercising the SSE endpoint from a browserdocs-src/— Hugo documentation sources (7 languages: cs, en, de, es, fr, it, pt)docs-src/static/demo/quickstart/— Hello World example (hello_world.sql,index.html) used by the Quick Start page; keep in sync with the snippets indocs-src/content/*/quickstart.htmldocs-src/i18n/*.yaml— localised hero section strings; rendered with| safeHTMLto allow<br>tagsdocs-src/layouts/index.html— home page template; uses| safeHTMLfor i18n hero fieldsdocs/— generated static documentation site.goreleaser.yaml— CLI release artifacts (darwin/linux/windows, amd64+arm64)scripts/generate_homebrew_formula.sh/generate_homebrew_cask.sh— Homebrew tap filesscripts/publish_release.sh— tags, pushes, creates the GitHub release, updates theheptau/tapHomebrew tap; invoked bymake releaseMakefile—make build,make tests(Docker + Go),make release-local(build+verify, no push),make release(release-local + publish),make docs
Testing
make tests— runs all tests: starts a Docker Postgres container, appliesschema.sql+mcp_functions.sql+seed_data.sql, executesgo test ./..., tears down.- Protocol-level MCP tests (no DB needed) run always.
- Integration tests skip unless
PGARACHNE_TEST_DB=1is set. - Test helpers:
mcpDo,decodeMCPResponse,newProtocolTestServer,insertTestPrompt,deleteTestPrompt.
Guidance for AI Assistance
- Keep solutions aligned with the JSON-RPC + PostgreSQL function model (single
jsonbparam). - Do not suggest bypassing DB role security; the role switching is intentional.
- When adding features, consider updates needed in both Go server and SQL files (
schema.sqland/ormcp_functions.sql). - New MCP methods backed by SQL: add a row to
mcpDatabaseMethodsinmcp.go+ implement the function inmcp_functions.sql. No other Go changes needed. - Prefer changes that preserve the minimal middleware footprint and performance.
- Keep release artifacts and docs in sync when changing file names/URLs.
- The MCP endpoint is a translation layer only — PostgreSQL functions remain JSON-RPC-shaped and know nothing about MCP.
- Documentation links in
_index.htmlmust use relative paths without language prefix (e.g.architectural-decisions/, notcs/architectural-decisions.html) because Hugo adds the language prefix automatically. - i18n strings in
docs-src/i18n/*.yamlthat contain HTML (e.g.<br>) require the template to use| safeHTML; this is already applied indocs-src/layouts/index.html. - For contributor-facing workflow and contribution conventions, see
CONTRIBUTING.md.
AI_CONTEXT Maintenance Rule
- This file should be updated automatically after meaningful architectural, security, release, routing, configuration, or documentation workflow changes.
- Do not wait for a special request when the update is clearly relevant.
SSE Shutdown Semantics
sseHub.Shutdown(ctx)drops all clients (signalsclient.done), closes everypq.Listener, and detaches thedbsmap. New SSE requests issued afterShutdownbegins see a fresh, empty hub.handleSSEincludescase <-client.done: returnso a server-driven drop is observed immediately. Without it the handler would only exit on idle timeout (default 30s) or client request cancellation.- Test strategy for shutdown: assert on the server-side invariant (
sseHub.dbsempty,sseDropsCounter{dbName,"shutdown"}incremented) rather than the client seeing EOF. Go'shttp.Serverkeeps the underlying TCP connection alive for keep-alive even after the chunked response terminator, so SSE clients will not see EOF until the idle timeout — that is by design and outside the scope of graceful shutdown.
Local Postgres.app trust Auth Caveat
- Postgres.app on macOS ships with
pg_hba.confset totrustfor localhost. Negative-login tests (TestLoginInvalidCredentials,TestLoginRateLimit) cannot distinguish wrong passwords on such a setup, so they are skipped viarequireEnforcedPasswordAuthwhen wrong-password login is accepted. CI (Docker Postgres) usesscram-sha-256and runs them normally.
Coverage & Benchmarks
make coverage— runsgo test -coverprofile=coverage.out -covermode=atomic ./.... Standalone (withoutPGARACHNE_TEST_DB) lands around 24%; withPGARACHNE_TEST_DB=1it climbs to ~60%. The integration suites coverinternal/server,internal/database, and the SQL-backed branches ofinternal/auth.make cover-html— convertscoverage.outtocoverage.html. Excluded from git via.gitignore.make bench— runsgo test -run='^$' -bench=. -benchmem ./internal/auth ./internal/config ./internal/server. Pure-CPU hot paths only (no DB). The Go test runner takes the-run='^$'filter so no test cases are executed — the bench is the only thing measured.- The CI workflow generates
coverage.outas part ofscripts/run_tests.sh(the integration-test step, while the Docker Postgres container is still up — a separate later step re-runninggo testwithPGARACHNE_TEST_DB=1would fail, since the container is torn down as soon as that script exits) and uploads it to Codecov (continue-on-error: trueso the workflow still passes when theCODECOV_TOKENsecret is missing). The README badge points athttps://codecov.io/gh/heptau/pgarachne.
OpenAPI Spec
pgarachne.generate_openapi_spec(server_url_base, db_name)is aSECURITY INVOKER(noSECURITY DEFINERclause — deliberately, see below)STABLEplpgsql function. It pulls the method list frompgarachne.capabilities()(the same source the JSON-RPCcapabilitiesmethod and MCPtools/listuse), so the spec stays in sync withpg_procand is filtered to what the calling role may execute. The doc is OpenAPI 3.1.0 and contains:info.titlederived fromCURRENT_CATALOG;info.summarystatic.servers[0].urlis the bareserver_url_base(origin only) — everypathskey is already a full absolute path, and OpenAPI 3.1 resolves an operation's URL asserver.url + path key, so the server entry must not also carry a path prefix or URLs built from the spec would double up.pathscontaining the real JSON-RPC POST operation at/{prefix}/{database}/jsonrpc, plus one virtual, documentation-only path per exposed method at/{prefix}/{database}/rpc/{method}(method names may contain dots, e.g.api.hello_world— used verbatim, no escaping needed). These/rpc/*paths do not exist as real HTTP routes; every actual call still goes through/jsonrpc, and each virtual operation'sdescriptionspells out the real JSON-RPC request needed to invoke it. This exists because tooling that expects one operation per path (Swagger UI, Postman, codegen) can't otherwise represent N method signatures under a single JSON-RPC path.x-pgarachne-methodsextension on the/jsonrpcPOST operation only: an array of{name, description, parameters}for every callable method, for tooling that understands the extension.- Standard response codes 200/400/401/403/404/409/429/500 with human descriptions, referencing shared
components.schemas.JsonRpcResponse/JsonRpcErrorschemas via$ref. components.schemas.JsonRpcRequest/JsonRpcResponse/JsonRpcError— reusable named schemas for the JSON-RPC envelope, referenced from both the/jsonrpcpath and the virtual per-method paths' responses.components.securitySchemes.BearerAuth(HTTP bearer) for both JWT and API tokens.
- The Go route
GET /{prefix}/:database/openapi.json(handleOpenAPISpec/handleOpenAPISpecFormatininternal/server/server.go) requires the same three-mode auth as/jsonrpc(Basic / Bearer JWT / API token), opens a tx, callssetupRequestTxtoSET LOCAL ROLEto the authenticated role, then calls the function with the request'sHostheader (so the spec matches the client's view of the public base URL) and serves the result asapplication/jsonwithCache-Control: no-cache. An unauthenticated request gets401, same as/jsonrpc. GET /{prefix}/:database/openapi.yaml(or?format=yamlon either route) returns the same document converted to YAML viagithub.com/goccy/go-yaml, served asapplication/yaml.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

