Schnellstart
Schnellstart
Erhalten Sie in wenigen Minuten eine funktionierende API für Ihre PostgreSQL-Datenbank — und rufen Sie sie von einer einfachen Webseite aus auf. Ihre Datenbank ist Ihr Backend: kein Framework, kein Boilerplate, keine JavaScript-Bibliothek. Wir bauen Schritt für Schritt ein „Hello World": eine Funktion mit Parameter, eine Echtzeit-Benachrichtigung und eine generierte Datei.
Der gesamte Code dieser Anleitung steht als fertiges Beispiel bereit:
hello_world.sql (Datenbank) und
index.html (Webseite).
Jeder Endpunkt ist auf seiner eigenen Referenzseite ausführlich beschrieben: JSON-RPC,
Realtime-Benachrichtigungen (SSE) und Datei-Download (/file).
1. Installation
Binärdatei herunterladen
# macOS (Homebrew)
brew install heptau/tap/pgarachne
# or download the latest release for your OS:
# https://github.com/heptau/pgarachne/releases/latest2. Datenbank einrichten
Legen Sie eine Datenbank an, die Service-Rolle, mit der sich PgArachne verbindet, und laden Sie das PgArachne-Schema
(sql/schema.sql liegt im Repository):
createdb my_database
psql -d my_database -c "CREATE ROLE pgarachne LOGIN PASSWORD 'pgarachne_password'"
psql -d my_database -f sql/schema.sql3. Konfigurieren & starten
Erstellen Sie eine Datei pgarachne.env (der Server findet sie im aktuellen Verzeichnis).
Mit STATIC_FILES_PATH liefert PgArachne auch die Webseite aus, die Sie in Schritt 5 erstellen, sodass Seite und
API denselben Origin teilen und keine CORS-Konfiguration nötig ist:
DB_HOST=localhost
DB_PORT=5432
DB_USER=pgarachne
DB_SSLMODE=disable # local PostgreSQL without TLS only
STATIC_FILES_PATH=/path/to/web # folder with your index.html
# Optional: enables JWT sessions (POST /db//token); generate with openssl rand -hex 32
# JWT_SECRET=… Starten Sie den Server (das Passwort der Service-Rolle wird aus PGPASSWORD oder ~/.pgpass gelesen):
PGPASSWORD=pgarachne_password ./pgarachne4. Hello World — eine Funktion mit Parameter
Jede API-Methode ist eine PostgreSQL-Funktion mit einem einzigen jsonb-Argument. Legen Sie eine Rolle für die Webseite
und die Funktion an; das JSON-Objekt, das ein Client als params sendet, kommt als payload an:
CREATE ROLE demo_user WITH LOGIN PASSWORD 'user_password';
GRANT USAGE ON SCHEMA api TO demo_user;
CREATE OR REPLACE FUNCTION api.hello_world(payload jsonb DEFAULT '{}'::jsonb)
RETURNS json LANGUAGE sql AS $$
SELECT json_build_object(
'message', 'Hello, ' || COALESCE(payload->>'name', 'World') || '!');
$$;
GRANT EXECUTE ON FUNCTION api.hello_world(jsonb) TO demo_user;Der Zugriff wird von PostgreSQL selbst gesteuert: demo_user kann genau die Funktionen aufrufen, für die er
EXECUTE erhalten hat — sonst ist nichts erreichbar.
5. Aufrufen — mit curl und von einer Webseite
Ihr API-Endpunkt ist live. Benutzername und Passwort der PostgreSQL-Rolle werden per HTTP-Basic-Authentifizierung gesendet:
curl -X POST http://localhost:8080/db/my_database/jsonrpc \
-u demo_user:user_password \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"api.hello_world","params":{"name":"Alice"},"id":1}'
# → {"jsonrpc":"2.0","result":{"message":"Hello, Alice!"},"id":1}Derselbe Aufruf aus dem Browser, mit dem eingebauten fetch(). Speichern Sie dies als index.html in dem Ordner
aus STATIC_FILES_PATH und öffnen Sie http://localhost:8080/:
<input id="name" value="Alice">
<button id="hello">Say hello</button>
<pre id="out"></pre>
<script>
// HTTP Basic header: "user:password" in base64 (TextEncoder handles non-ASCII)
const bytes = new TextEncoder().encode("demo_user:user_password");
const auth = "Basic " + btoa(String.fromCharCode(...bytes));
const base = location.origin + "/db/my_database"; // /{API_PREFIX}/{database}
async function rpc(method, params) {
const res = await fetch(base + "/jsonrpc", {
method: "POST",
headers: { "Authorization": auth, "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", method, params, id: 1 }),
});
const body = await res.json();
if (!res.ok || body.error) throw new Error(body.error.message);
return body.result;
}
document.getElementById("hello").onclick = async () => {
const result = await rpc("api.hello_world", { name: document.getElementById("name").value });
document.getElementById("out").textContent = result.message; // Hello, Alice!
};
</script>6. Echtzeit — die Datenbank eine Nachricht senden lassen (SSE)
PostgreSQL kann Nachrichten mit pg_notify veröffentlichen; PgArachne leitet sie als Server-Sent Events an Browser weiter.
Erweitern Sie die Funktion: Übergibt der Aufrufer "notify": true, veröffentlicht sie die Begrüßung zusätzlich
auf dem Kanal hello:
CREATE OR REPLACE FUNCTION api.hello_world(payload jsonb DEFAULT '{}'::jsonb)
RETURNS json LANGUAGE plpgsql AS $$
DECLARE
greeting text := 'Hello, ' || COALESCE(payload->>'name', 'World') || '!';
BEGIN
IF COALESCE((payload->>'notify')::boolean, false) THEN
PERFORM pg_notify('hello', json_build_object('message', greeting)::text);
END IF;
RETURN json_build_object('message', greeting);
END; $$;Abonnieren Sie in der Seite zuerst und rufen Sie dann die Funktion auf. Das eingebaute EventSource kann keinen
Authorization-Header senden, daher wird der Stream mit fetch() gelesen (etwa 15 Zeilen):
async function listen(channels, onMessage) {
const res = await fetch(base + "/sse?channels=" + encodeURIComponent(channels), {
headers: { "Authorization": auth },
});
if (!res.ok) throw new Error((await res.json()).error);
const reader = res.body.getReader(), decoder = new TextDecoder();
let buffer = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let end;
while ((end = buffer.indexOf("\n\n")) >= 0) { // an event ends with a blank line
const event = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
for (const line of event.split("\n"))
if (line.startsWith("data: ")) onMessage(JSON.parse(line.slice(6)));
}
}
}
listen("hello", (m) => console.log("event:", m.data.message)); // m = {channel, data}
rpc("api.hello_world", { name: "Alice", notify: true }); // the event arrives immediatelyNOTIFY ist transaktional — PostgreSQL liefert es aus, wenn die Transaktion der Funktion
committet, also genau dann, wenn der Aufruf zurückkehrt. Abonnieren Sie vor dem Aufruf: PostgreSQL hebt
Benachrichtigungen für Listener, die noch nicht verbunden sind, nicht auf. SSE-Kanäle sind nicht rollenbasiert eingeschränkt
(siehe Realtime-Benachrichtigungen (SSE)).7. Eine Datei herunterladen
Eine Funktion, die Zeilen (path, content, mime_type, store_only) zurückgibt, kann über den Endpunkt
/file heruntergeladen werden — als rohe Bytes, ohne Base64 innerhalb von JSON. Diese hier liefert hello-world.md:
CREATE OR REPLACE FUNCTION api.hello_world_file(payload jsonb DEFAULT '{}'::jsonb)
RETURNS TABLE (path text, content bytea, mime_type text, store_only boolean)
LANGUAGE sql AS $$
SELECT 'hello-world.md',
convert_to('# Hello, ' || COALESCE(payload->>'name', 'World') || E'!\n\nGenerated by PostgreSQL at ' || now() || E'.\n', 'UTF8'),
'text/markdown',
false;
$$;
GRANT EXECUTE ON FUNCTION api.hello_world_file(jsonb) TO demo_user;In der Seite können Sie den Inhalt anzeigen oder als Download anbieten (geben Sie mehr Zeilen zurück, erhalten Sie automatisch ein ZIP):
async function getFile() {
const res = await fetch(base + "/file", {
method: "POST",
headers: { "Authorization": auth, "Content-Type": "application/json" },
body: JSON.stringify({ method: "api.hello_world_file", params: { name: "Alice" } }),
});
if (!res.ok) throw new Error((await res.json()).error.message); // errors are JSON, never binary
return res.blob();
}
// show it:
console.log(await (await getFile()).text());
// or download it:
const url = URL.createObjectURL(await getFile());
const a = Object.assign(document.createElement("a"), { href: url, download: "hello-world.md" });
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);Probieren Sie es mit curl aus: curl -u demo_user:user_password -H "Content-Type: application/json" -d '{"method":"api.hello_world_file"}' http://localhost:8080/db/my_database/file -OJ.
Mehr unter Datei-Download (/file).
Bevor Sie in Produktion gehen
- Verwenden Sie HTTPS. Basic-Zugangsdaten werden mit jeder Anfrage gesendet; siehe Bereitstellung & HTTPS. Ändern Sie die Demo-Passwörter.
- Zugangsdaten nur im Arbeitsspeicher halten (eine JavaScript-Variable), niemals in
localStorage. Um das Passwort nicht ständig erneut zu senden, setzen SieJWT_SECRETund fordern ein Token überPOST /db/my_database/tokenan (siehe Sicherheit & Authentifizierung). - Webseite auf einem anderen Origin (CDN, anderer Port)? Setzen Sie
ALLOWED_ORIGINS=https://app.example.com; Cross-Origin-Anfragen sind standardmäßig blockiert. - Minimale Rechte: eine PostgreSQL-Rolle pro Benutzerart,
EXECUTEnur auf die benötigten Funktionen, Row-Level Security für benutzerspezifische Daten undALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC, damit neue Funktionen nicht versehentlich freigegeben werden.
8. KI-Agent anbinden (MCP)
Stellen Sie dieselben Funktionen Claude Desktop oder Cursor als Tools über den MCP-Endpunkt bereit:
POST http://localhost:8080/db/my_database/mcpVollständige Verbindungsanleitungen finden Sie unter Model Context Protocol (MCP).
9. OpenAPI-Spezifikation abrufen (Swagger, Postman, Codegenerierung)
Jede bereitgestellte Funktion wird zudem als OpenAPI-3.1-Dokument beschrieben, gefiltert auf das, was der authentifizierte Aufrufer aufrufen darf:
curl http://localhost:8080/db/my_database/openapi.json \
-u demo_user:user_password
# or as YAML: .../openapi.yamlImportieren Sie die URL direkt in Swagger UI, Postman oder einen OpenAPI-Codegenerator. Jede Methode läuft weiterhin über den einen JSON-RPC-Endpunkt aus Schritt 5 — warum die Spezifikation einen Pfad pro Methode ohne passende echte Route auflistet, erklären die Architektur-Entscheidungen.
Das war’s. Lesen Sie die JSON-RPC-Referenz zu Anfrageformat, Funktionen schreiben, Fehlern und Idempotenz oder den Vergleich mit PostgREST, um zu verstehen, wo PgArachne einzuordnen ist.