Démarrage rapide
Démarrage rapide
Obtenez en quelques minutes une API fonctionnelle à partir de votre base de données PostgreSQL — et appelez-la depuis une simple page web. Votre base de données est votre backend : pas de framework, pas de code superflu, pas de bibliothèque JavaScript. Nous construisons un « Hello World» pas à pas : une fonction avec un paramètre, une notification en temps réel et un fichier généré.
Tout le code de ce guide est disponible sous forme d’exemple terminé :
hello_world.sql (base de données) et
index.html (page web).
Chaque point d’accès est décrit en détail sur sa propre page de référence : JSON-RPC,
Notifications temps réel (SSE) et Téléchargement de fichiers (/file).
1. Installation
Télécharger un binaire
# macOS (Homebrew)
brew install heptau/tap/pgarachne
# or download the latest release for your OS:
# https://github.com/heptau/pgarachne/releases/latest2. Préparer votre base de données
Créez une base de données, le rôle de service sous lequel PgArachne se connecte, puis chargez le schéma PgArachne
(sql/schema.sql se trouve dans le dépôt) :
createdb my_database
psql -d my_database -c "CREATE ROLE pgarachne LOGIN PASSWORD 'pgarachne_password'"
psql -d my_database -f sql/schema.sql3. Configurer et démarrer
Créez un fichier pgarachne.env (le serveur le trouve dans le répertoire courant).
STATIC_FILES_PATH permet à PgArachne de servir également la page web que vous créerez à l’étape 5, de sorte que la page et
l’API partagent la même origine et qu’aucune configuration CORS n’est nécessaire :
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=… Démarrez le serveur (le mot de passe du rôle de service est lu depuis PGPASSWORD ou ~/.pgpass) :
PGPASSWORD=pgarachne_password ./pgarachne4. Hello World — une fonction avec un paramètre
Chaque méthode de l’API est une fonction PostgreSQL avec un seul argument jsonb. Créez un rôle pour la page web et
la fonction ; l’objet JSON qu’un client envoie comme params arrive sous le nom 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’accès est contrôlé par PostgreSQL lui-même : demo_user peut appeler exactement les fonctions pour lesquelles il a reçu
EXECUTE — rien d’autre n’est accessible.
5. L’appeler — avec curl et depuis une page web
Votre point d’accès API est actif. Le nom d’utilisateur et le mot de passe du rôle PostgreSQL sont envoyés avec l’authentification 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}Le même appel depuis un navigateur, avec le fetch() intégré. Enregistrez ceci sous index.html dans le dossier
indiqué par STATIC_FILES_PATH et ouvrez 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. Temps réel — laisser la base de données pousser un message (SSE)
PostgreSQL peut publier des messages avec pg_notify ; PgArachne les transmet aux navigateurs sous forme de Server-Sent Events.
Étendez la fonction : lorsque l’appelant passe "notify": true, elle publie en plus le message d’accueil
sur le canal 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; $$;Dans la page, abonnez-vous d’abord, puis appelez la fonction. Le EventSource intégré ne peut pas envoyer d’en-tête
Authorization ; le flux est donc lu avec fetch() (environ 15 lignes) :
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 est transactionnel — PostgreSQL le délivre lorsque la transaction
de la fonction est validée (commit), c’est-à-dire au moment même où l’appel retourne. Abonnez-vous avant d’appeler : PostgreSQL ne conserve pas
les notifications pour les écouteurs qui ne sont pas encore connectés. Les canaux SSE ne sont pas limités par rôle
(voir Notifications temps réel (SSE)).7. Télécharger un fichier
Une fonction qui renvoie des lignes (path, content, mime_type, store_only) peut être téléchargée via le point d’accès
/file — sous forme d’octets bruts, sans base64 dans du JSON. Celle-ci renvoie 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;Dans la page, vous pouvez afficher le contenu ou le proposer en téléchargement (si vous renvoyez plusieurs lignes, vous obtenez automatiquement un 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);Essayez avec 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.
Plus de détails dans Téléchargement de fichiers (/file).
Avant la mise en production
- Utilisez HTTPS. Les identifiants Basic circulent à chaque requête ; voir Déploiement & HTTPS. Changez les mots de passe de démonstration.
- Gardez les identifiants uniquement en mémoire (une variable JavaScript), jamais dans
localStorage. Pour éviter de renvoyer le mot de passe, définissezJWT_SECRETet demandez un jeton àPOST /db/my_database/token(voir Sécurité & Authentification). - Page web sur une autre origine (un CDN, un autre port) ? Définissez
ALLOWED_ORIGINS=https://app.example.com; les requêtes cross-origin sont bloquées par défaut. - Moindre privilège : un rôle PostgreSQL par type d’utilisateur,
EXECUTEuniquement sur les fonctions nécessaires, Row-Level Security pour les données propres à chaque utilisateur etALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLICpour que les nouvelles fonctions ne soient pas exposées par accident.
8. Connecter un agent IA (MCP)
Exposez les mêmes fonctions à Claude Desktop ou Cursor en tant qu’outils via le point d’accès MCP :
POST http://localhost:8080/db/my_database/mcpConsultez Model Context Protocol (MCP) pour les instructions de connexion complètes.
9. Obtenir une spécification OpenAPI (Swagger, Postman, génération de code)
Chaque fonction exposée est également décrite dans un document OpenAPI 3.1, filtré selon ce que l’appelant authentifié peut appeler :
curl http://localhost:8080/db/my_database/openapi.json \
-u demo_user:user_password
# or as YAML: .../openapi.yamlImportez l’URL directement dans Swagger UI, Postman ou un générateur de code OpenAPI. Chaque méthode passe toujours par l’unique point d’accès JSON-RPC de l’étape 5 — voir Décisions d’Architecture pour comprendre pourquoi la spécification liste un chemin par méthode sans route réelle correspondante.
Voilà. Lisez la référence JSON-RPC pour le format des requêtes, l’écriture de fonctions, les erreurs et l’idempotence, ou la comparaison avec PostgREST pour comprendre où PgArachne se situe.