Avvio rapido
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/latest2. 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.sql3. 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 ./pgarachne4. 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 immediatelyNOTIFY è 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, impostaJWT_SECRETe richiedi un token aPOST /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,
EXECUTEsolo sulle funzioni necessarie, Row-Level Security per i dati per utente eALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLICperché 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/mcpConsulta 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.yamlImporta 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.