Início rápido

7 min de leitura

Início rápido

Tenha uma API funcional a partir do seu banco de dados PostgreSQL em poucos minutos — e chame-a de uma página web simples. Seu banco de dados é o seu backend: sem framework, sem código repetitivo, sem biblioteca JavaScript. Construímos um «Hello World» passo a passo: uma função com um parâmetro, uma notificação em tempo real e um arquivo gerado.

Todo o código deste guia está disponível como exemplo pronto: hello_world.sql (banco de dados) e index.html (página web). Cada endpoint é descrito em detalhe em sua própria página de referência: JSON-RPC, Notificações em tempo real (SSE) e Download de arquivos (/file).

1. Instalar

Baixar um binário

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

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

2. Prepare seu banco de dados

Crie um banco de dados, o papel de serviço com o qual o PgArachne se conecta e carregue o esquema do PgArachne (sql/schema.sql está no repositório):

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

Crie um arquivo pgarachne.env (o servidor o encontra no diretório atual). STATIC_FILES_PATH permite que o PgArachne sirva também a página web que você criará no passo 5, de modo que a página e a API compartilhem a mesma origem e nenhuma configuração de CORS seja necessária:

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

Inicie o servidor (a senha do papel de serviço é lida de PGPASSWORD ou de ~/.pgpass):

PGPASSWORD=pgarachne_password ./pgarachne

4. Hello World — uma função com um parâmetro

Cada método da API é uma função do PostgreSQL com um único argumento jsonb. Crie um papel para a página web e a função; o objeto JSON que um cliente envia como params chega 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;

O acesso é controlado pelo próprio PostgreSQL: demo_user pode chamar exatamente as funções nas quais recebeu EXECUTE — nada mais é acessível.

5. Chame-a — do curl e de uma página web

Seu endpoint de API já está no ar. O nome de usuário e a senha do papel do PostgreSQL são enviados com autenticação 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}

A mesma chamada de um navegador, com o fetch() nativo. Salve isto como index.html na pasta indicada em STATIC_FILES_PATH e abra 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 real — deixe o banco de dados enviar uma mensagem (SSE)

O PostgreSQL pode publicar mensagens com pg_notify; o PgArachne as encaminha aos navegadores como Server-Sent Events. Estenda a função: quando o chamador passa "notify": true, ela também publica a saudação no 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; $$;

Na página, inscreva-se primeiro e depois chame a função. O EventSource nativo não consegue enviar um cabeçalho Authorization, então o fluxo é lido com fetch() (cerca de 15 linhas):

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
Bom saber: NOTIFY é transacional — o PostgreSQL o entrega quando a transação da função é confirmada, ou seja, justamente quando a chamada retorna. Inscreva-se antes de chamar: o PostgreSQL não guarda notificações para ouvintes que ainda não estão conectados. Os canais SSE não são restritos por papel (veja Notificações em tempo real (SSE)).

7. Baixar um arquivo

Uma função que retorna linhas (path, content, mime_type, store_only) pode ser baixada pelo endpoint /file — como bytes brutos, sem base64 dentro do JSON. Esta retorna 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;

Na página você pode exibir o conteúdo ou oferecê-lo como download (se retornar mais linhas, você recebe um ZIP automaticamente):

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

Experimente com 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. Mais em Download de arquivos (/file).

Antes de ir para produção

  • Use HTTPS. As credenciais Basic viajam a cada requisição; veja Deployment e HTTPS. Troque as senhas de demonstração.
  • Mantenha as credenciais apenas na memória (uma variável JavaScript), nunca em localStorage. Para evitar reenviar a senha, defina JWT_SECRET e solicite um token a POST /db/my_database/token (veja Segurança e Autenticação).
  • Página web em outra origem (uma CDN, outra porta)? Defina ALLOWED_ORIGINS=https://app.example.com; requisições cross-origin são bloqueadas por padrão.
  • Privilégio mínimo: um papel PostgreSQL por tipo de usuário, EXECUTE apenas nas funções necessárias, Row-Level Security para dados por usuário e ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC para que novas funções não sejam expostas por acidente.

8. Conecte um agente de IA (MCP)

Exponha as mesmas funções como ferramentas ao Claude Desktop ou ao Cursor por meio do endpoint MCP:

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

Veja Model Context Protocol (MCP) para as instruções completas de conexão.

9. Obtenha uma especificação OpenAPI (Swagger, Postman, codegen)

Cada função exposta também é descrita como um documento OpenAPI 3.1, filtrado pelo que o chamador autenticado pode invocar:

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

Importe a URL diretamente no Swagger UI, no Postman ou em um gerador de código OpenAPI. Cada método continua sendo executado pelo único endpoint JSON-RPC do passo 5 — veja Decisões de Arquitetura para entender por que a especificação lista um caminho por método sem uma rota real correspondente.

É isso. Leia a referência JSON-RPC para o formato de requisição, escrita de funções, erros e idempotência, ou a Comparação: PgArachne vs. PostgREST para entender onde o PgArachne se encaixa.