Inicio rápido

7 min de lectura

Inicio rápido

Obtén una API funcional a partir de tu base de datos PostgreSQL en pocos minutos — y llámala desde una página web sencilla. Tu base de datos es tu backend: sin framework, sin código repetitivo, sin librerías JavaScript. Construimos un «Hello World» paso a paso: una función con un parámetro, una notificación en tiempo real y un archivo generado.

Todo el código de esta guía está disponible como ejemplo terminado: hello_world.sql (base de datos) y index.html (página web). Cada endpoint se describe en detalle en su propia página de referencia: JSON-RPC, Notificaciones en tiempo real (SSE) y Descarga de archivos (/file).

1. Instalar

Descargar 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. Configura tu base de datos

Crea una base de datos, el rol de servicio con el que se conecta PgArachne y carga el esquema de PgArachne (sql/schema.sql está en el repositorio):

createdb my_database
psql -d my_database -c "CREATE ROLE pgarachne LOGIN PASSWORD 'pgarachne_password'"
psql -d my_database -f sql/schema.sql

3. Configurar e iniciar

Crea un archivo pgarachne.env (el servidor lo encuentra en el directorio actual). STATIC_FILES_PATH permite que PgArachne sirva también la página web que crearás en el paso 5, de modo que la página y la API comparten el mismo origen y no hace falta configurar 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=…

Inicia el servidor (la contraseña del rol de servicio se lee de PGPASSWORD o de ~/.pgpass):

PGPASSWORD=pgarachne_password ./pgarachne

4. Hello World — una función con un parámetro

Cada método de la API es una función de PostgreSQL con un único argumento jsonb. Crea un rol para la página web y la función; el objeto JSON que un cliente envía como params llega como 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;

El acceso lo controla el propio PostgreSQL: demo_user puede llamar exactamente a las funciones sobre las que se le concedió EXECUTE — nada más es accesible.

5. Llámala — desde curl y desde una página web

Tu endpoint de API ya está activo. El nombre de usuario y la contraseña del rol de PostgreSQL se envían con autenticación 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 misma llamada desde un navegador, con el fetch() integrado. Guarda esto como index.html en la carpeta indicada en STATIC_FILES_PATH y abre 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. Tiempo real — que la base de datos envíe un mensaje (SSE)

PostgreSQL puede publicar mensajes con pg_notify; PgArachne los reenvía a los navegadores como Server-Sent Events. Amplía la función: cuando el llamador pasa "notify": true, también publica el saludo en el 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; $$;

En la página, suscríbete primero y luego llama a la función. El EventSource integrado no puede enviar una cabecera Authorization, así que el flujo se lee con fetch() (unas 15 líneas):

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
Conviene saber: NOTIFY es transaccional — PostgreSQL lo entrega cuando se confirma la transacción de la función, es decir, justo cuando la llamada devuelve su respuesta. Suscríbete antes de llamar: PostgreSQL no guarda las notificaciones para oyentes que aún no están conectados. Los canales SSE no están limitados por rol (consulta Notificaciones en tiempo real (SSE)).

7. Descargar un archivo

Una función que devuelve filas (path, content, mime_type, store_only) puede descargarse a través del endpoint /file — como bytes sin procesar, sin base64 dentro de JSON. Esta devuelve 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;

En la página puedes mostrar el contenido u ofrecerlo como descarga (si devuelves más filas, obtienes un ZIP automáticamente):

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);

Pruébalo 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. Más en Descarga de archivos (/file).

Antes de pasar a producción

  • Usa HTTPS. Las credenciales Basic viajan con cada petición; consulta Despliegue y HTTPS. Cambia las contraseñas de demostración.
  • Mantén las credenciales solo en memoria (una variable de JavaScript), nunca en localStorage. Para no reenviar la contraseña, configura JWT_SECRET y solicita un token a POST /db/my_database/token (consulta Seguridad y Autenticación).
  • ¿Página web en otro origen (un CDN, otro puerto)? Configura ALLOWED_ORIGINS=https://app.example.com; las peticiones de origen cruzado están bloqueadas por defecto.
  • Mínimo privilegio: un rol de PostgreSQL por tipo de usuario, EXECUTE solo en las funciones que necesita, Row-Level Security para datos por usuario y ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC para que las funciones nuevas no se expongan por accidente.

8. Conecta un agente de IA (MCP)

Expón las mismas funciones como herramientas a Claude Desktop o Cursor mediante el endpoint MCP:

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

Consulta Model Context Protocol (MCP) para las instrucciones completas de conexión.

9. Obtén una especificación OpenAPI (Swagger, Postman, codegen)

Cada función expuesta se describe también como un documento OpenAPI 3.1, filtrado según lo que el llamador autenticado puede invocar:

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

Importa la URL directamente en Swagger UI, Postman o un generador de código OpenAPI. Cada método sigue ejecutándose a través del único endpoint JSON-RPC del paso 5 — consulta Decisiones de Arquitectura para saber por qué la especificación lista una ruta por método sin una ruta real equivalente.

Eso es todo. Lee la referencia JSON-RPC para el formato de petición, la escritura de funciones, los errores y la idempotencia, o la Comparación: PgArachne vs. PostgREST para entender dónde encaja PgArachne.