Avvio rapido

7 min di lettura

Avvio rapido

Ottieni un’API funzionante dal tuo database PostgreSQL in pochi minuti — e chiamala da una semplice pagina web. Il tuo database è il tuo backend: nessun framework, nessun boilerplate, nessuna libreria JavaScript. Costruiamo passo passo un «Hello World»: una funzione con un parametro, una notifica in tempo reale e un file generato.

Tutto il codice di questa guida è disponibile come esempio completo: hello_world.sql (database) e index.html (pagina web). Ogni endpoint è descritto in dettaglio nella sua pagina di riferimento: JSON-RPC, Notifiche in tempo reale (SSE) e Download di file (/file).

1. Installazione

Scaricare un binario

# macOS (Homebrew)
brew install heptau/tap/pgarachne

# or download the latest release for your OS:
#   https://github.com/heptau/pgarachne/releases/latest

2. Prepara il database

Crea un database, il ruolo di servizio con cui si connette PgArachne e carica lo schema di PgArachne (sql/schema.sql si trova nel 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. Configura e avvia

Crea un file pgarachne.env (il server lo trova nella directory corrente). STATIC_FILES_PATH permette a PgArachne di servire anche la pagina web che creerai al passo 5, così la pagina e l’API condividono la stessa origine e non serve alcuna configurazione CORS:

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=…

Avvia il server (la password del ruolo di servizio viene letta da PGPASSWORD o da ~/.pgpass):

PGPASSWORD=pgarachne_password ./pgarachne

4. Hello World — una funzione con un parametro

Ogni metodo dell’API è una funzione PostgreSQL con un solo argomento jsonb. Crea un ruolo per la pagina web e la funzione; l’oggetto JSON che un client invia come params arriva come payload:

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;

L’accesso è controllato da PostgreSQL stesso: demo_user può chiamare esattamente le funzioni su cui ha ricevuto EXECUTE — nient’altro è raggiungibile.

5. Chiamala — da curl e da una pagina web

Il tuo endpoint API è attivo. Nome utente e password del ruolo PostgreSQL vengono inviati con l’autenticazione HTTP Basic:

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}

La stessa chiamata da un browser, con il fetch() integrato. Salva questo come index.html nella cartella indicata in STATIC_FILES_PATH e apri 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. Tempo reale — lascia che il database invii un messaggio (SSE)

PostgreSQL può pubblicare messaggi con pg_notify; PgArachne li inoltra ai browser come Server-Sent Events. Estendi la funzione: quando il chiamante passa "notify": true, pubblica anche il saluto sul canale 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; $$;

Nella pagina, iscriviti prima e poi chiama la funzione. L’EventSource integrato non può inviare un header Authorization, quindi lo stream viene letto con fetch() (circa 15 righe):

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
Da sapere: NOTIFY è transazionale — PostgreSQL lo consegna quando viene eseguito il commit della transazione della funzione, cioè proprio quando la chiamata restituisce la risposta. Iscriviti prima di chiamare: PostgreSQL non conserva le notifiche per i listener non ancora connessi. I canali SSE non sono limitati per ruolo (vedi Notifiche in tempo reale (SSE)).

7. Scaricare un file

Una funzione che restituisce righe (path, content, mime_type, store_only) può essere scaricata tramite l’endpoint /file — come byte grezzi, senza base64 dentro il JSON. Questa restituisce 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;

Nella pagina puoi mostrare il contenuto oppure offrirlo come download (restituendo più righe ottieni automaticamente uno 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);

Provalo con curl: 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. Altro in Download di file (/file).

Prima di andare in produzione

  • Usa HTTPS. Le credenziali Basic viaggiano con ogni richiesta; vedi Deployment e HTTPS. Cambia le password di esempio.
  • Tieni le credenziali solo in memoria (una variabile JavaScript), mai in localStorage. Per evitare di reinviare la password, imposta JWT_SECRET e richiedi un token a POST /db/my_database/token (vedi Sicurezza e Autenticazione).
  • Pagina web su un altro origin (una CDN, un’altra porta)? Imposta ALLOWED_ORIGINS=https://app.example.com; le richieste cross-origin sono bloccate per impostazione predefinita.
  • Privilegio minimo: un ruolo PostgreSQL per ogni tipo di utente, EXECUTE solo sulle funzioni necessarie, Row-Level Security per i dati per utente e ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC perché le nuove funzioni non vengano esposte per errore.

8. Collega un agente AI (MCP)

Esponi le stesse funzioni come strumenti a Claude Desktop o Cursor tramite l’endpoint MCP:

POST http://localhost:8080/db/my_database/mcp

Consulta Model Context Protocol (MCP) per le istruzioni complete di connessione.

9. Ottieni una specifica OpenAPI (Swagger, Postman, codegen)

Ogni funzione esposta è descritta anche come documento OpenAPI 3.1, filtrato in base a ciò che il chiamante autenticato può invocare:

curl http://localhost:8080/db/my_database/openapi.json \
  -u demo_user:user_password
# or as YAML: .../openapi.yaml

Importa l’URL direttamente in Swagger UI, Postman o in un generatore di codice OpenAPI. Ogni metodo viene comunque eseguito tramite l’unico endpoint JSON-RPC del passo 5 — vedi Decisioni Architetturali per capire perché la specifica elenca un percorso per metodo senza una route reale corrispondente.

Tutto qui. Leggi il riferimento JSON-RPC per il formato delle richieste, la scrittura di funzioni, gli errori e l’idempotenza, oppure il Confronto: PgArachne vs. PostgREST per capire dove si colloca PgArachne.