Schnellstart

6 Min. Lesezeit

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/latest

2. 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.sql

3. 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 ./pgarachne

4. 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 immediately
Gut zu wissen: NOTIFY 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 Sie JWT_SECRET und fordern ein Token über POST /db/my_database/token an (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, EXECUTE nur auf die benötigten Funktionen, Row-Level Security für benutzerspezifische Daten und ALTER 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/mcp

Vollstä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.yaml

Importieren 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.