Início rápido
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/latest2. 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.sql3. 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 ./pgarachne4. 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 immediatelyNOTIFY é 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, definaJWT_SECRETe solicite um token aPOST /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,
EXECUTEapenas nas funções necessárias, Row-Level Security para dados por usuário eALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLICpara 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/mcpVeja 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.yamlImporte 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.