Documentação

Referência completa da plataforma. Use a navegação à esquerda ou Ctrl/Cmd+F pra busca rápida.

Visão geral

O que é o InfraHub e como funciona

O InfraHub é uma plataforma self-hostedque provisiona backend completo (Postgres + Auth + Storage + Realtime) por projeto. Você cria um projeto pelo painel, recebe credenciais, e a sua aplicação consome via SDK ou REST — igual Supabase, mas rodando na sua infra.

Conceitos

  • Projeto: unidade de isolamento. Cada projeto tem banco Postgres dedicado, bucket MinIO próprio, JWT secret próprio.
  • API key (if_xxx...): identifica o projeto. Vai no header x-infraforge-keyde toda request server-side. Pode ficar no app cliente (não é segredo de produção isolado — mas não exponha pra atacantes desnecessariamente).
  • JWT secret: assina os tokens dos end-users. Backend do InfraHub usa pra validar JWT em cada query. Nunca deve sair do InfraHub nem do servidor do app que precisar emitir tokens externos.
  • RLS (Row-Level Security): políticas SQL que filtram linhas por usuário. InfraHub injeta request.jwt.sub / role / email em cada query do SDK, e suas policies referenciam auth.uid() / auth.role().

Fluxo típico

1. Usuário cria projeto no painel → recebe API key + JWT secret
2. App instala o SDK e configura com (URL + slug + apiKey)
3. End-user faz signUp / signIn → recebe JWT
4. SDK injeta JWT no Authorization de cada query
5. Backend valida JWT, aplica RLS, retorna só linhas que o user pode ver

Quickstart

Do zero ao primeiro signUp + query em ~5 minutos

1. Criar projeto

No painel, vai em Projetos → Novo projeto. Escolhe modo:

  • managed_local: InfraHub spawna container Postgres+MinIO na própria VPS. Sem setup, mais rápido.
  • external (BYODB): você fornece a connection URL do seu Postgres. Veja BYO Database antes.
  • managed_remote: InfraHub spawna container numa VPS sua (cadastrada em Minhas VPSs) via SSH.

Após o provisionamento (~10-30s), você recebe API key(formato if_xxx...). Copia essa key.

2. Instalar SDK

Instalar: Node 18+

npm install @infraforge/sdk
ts

3. Inicializar cliente

import { createClient } from '@infraforge/sdk'

const client = createClient({
  url: 'https://infraforge.seudominio.com',
  projectSlug: 'meu-projeto',
  apiKey: process.env.INFRAFORGE_API_KEY!,
})
ts

4. Primeiro signUp + query

// Cadastra usuário (default: pending de aprovação manual)
const { user } = await client.auth.signUp({
  email: 'alice@example.com',
  password: 'senha-forte-aqui',
})

// Após admin aprovar, faz login
await client.auth.signIn({ email: 'alice@example.com', password: 'senha-forte-aqui' })

// Cria tabela (use o painel SQL Editor pra rodar isso 1×)
//   CREATE TABLE posts (id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
//                       title text, author_id uuid REFERENCES auth.users(id));
//   ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
//   CREATE POLICY "own posts" ON posts USING (author_id = auth.uid());

// Insert + query (RLS aplicado automático via JWT)
await client.db.query("INSERT INTO posts (title, author_id) VALUES ($1, auth.uid())", ['Olá!'])
const { data } = await client.db.query('SELECT * FROM posts')
console.log(data) // só os posts do usuário logado
ts

Próximo: as seções Autenticação, Database e Storage cobrem cada módulo em detalhe.

Autenticação

signUp, signIn, OAuth, MFA, sessões

End-users do seu app são gerenciados pelo InfraHub: cadastro, login, OAuth social, MFA, reset de senha. JWT assinado com o jwtSecret do projeto vai em todo request — backend valida em cada chamada.

Cadastro e login com email/senha

// Cadastro — default vai pra "pending approval" (admin libera no painel)
const { user, pending } = await client.auth.signUp({
  email: 'alice@example.com',
  password: 'senha-forte',
})

// Login (após aprovação)
const { token, error } = await client.auth.signIn({
  email: 'alice@example.com',
  password: 'senha-forte',
})

// Logout — limpa sessão local
client.auth.signOut()

// Verificar usuário atual (faz request server-side)
const { user } = await client.auth.getUser()

// Sessão local (sem rede)
const session = client.auth.getSession()
if (session) console.log(session.user.email)
ts

Modos de aprovação (configura no painel do projeto, aba Auth): manual_approval (default — admin aprova), email_verification (link no email confirma), auto_approve (libera direto, use com cuidado).

OAuth social (Google, GitHub)

Configura credenciais do provider no painel do projeto (aba Auth → Providers). SDK lida com o redirect + callback.

// Inicia o fluxo (browser redireciona pro provider)
client.auth.signInWithSocial({
  provider: 'google',           // ou 'github'
  redirectTo: '/dashboard',     // path relativo OU mesma origin (rejeita externos)
})

// Na sua página /callback, rode handleOAuthCallback
const result = client.auth.handleOAuthCallback()
if (result?.user) {
  // Logado! result.token tem o JWT
} else if (result?.error) {
  console.error('OAuth falhou:', result.error)
}
ts

Segurança: o SDK gera um state random salvo em sessionStorage antes do redirect, e valida no callback. Previne session fixation via link malicioso #token=....

MFA / TOTP no painel

MFA com TOTP (Google Authenticator, Authy, 1Password) está disponível pra contas do painel InfraHub (admin/tenant), não pros end-users do projeto. Ative em Conta → Segurança.

MFA pra end-users do projeto não está implementado ainda — está no roadmap. Se você precisa, abra uma issue.

Reset de senha

// User esqueceu — manda link por email (sempre retorna ok pra não vazar emails)
await client.auth.requestPasswordReset({ email: 'alice@example.com' })

// User clica no link → sua página /reset-password recebe ?token=...
// SDK não tem helper pronto — você posta direto:
await fetch(`${url}/api/auth/${slug}/reset-password`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ token, newPassword: 'nova-senha' }),
})
ts

setToken — quando você precisa

Útil quando o JWT chega de outro fluxo (callback OAuth externo, sistema legado, server-side handoff).

// Lança erro se JWT mal-formado, expirado, ou sem 'sub'.
// Não cria sessão "vazia" — UI nunca fica em estado pseudo-logada.
try {
  client.auth.setToken(jwt)
} catch (err) {
  console.error('JWT inválido:', err.message)
}

Onde a sessão fica no browser

Por default, sessão (JWT) é persistida em localStorage. Pra apps mais sensíveis, escolha modo:

createClient({
  url, projectSlug, apiKey,
  session: 'localStorage',     // (default) persiste entre tabs e refresh
  // session: 'sessionStorage',// só vive enquanto a aba estiver aberta
  // session: 'memory',        // nada gravado; refresh = re-login
})

memory é imune a XSS de leitura, mas pior UX. Use pra apps com dados muito sensíveis.

Database & queries

SQL via SDK com RLS automático

SDK envia SQL pro endpoint /api/query/[slug]. Backend valida JWT, abre transação, injeta request.jwt.sub/role/email, executa, devolve rows. Tudo passa por RLS.

Query parametrizada

// Sempre use $1, $2 — concatenação de string vira SQL injection
const { data, error } = await client.db.query<{ id: string; title: string }>(
  'SELECT id, title FROM posts WHERE author_id = $1 ORDER BY created_at DESC LIMIT $2',
  [userId, 10],
)
if (error) throw new Error(error)
console.log(data)
ts

RLS — funções helper disponíveis

O backend popula 3 functions no schema auth que suas policies usam:

auth.uid()    -- UUID do user logado (request.jwt.sub)
auth.role()   -- role do JWT (default 'authenticated')
auth.email()  -- email do user
sql

Exemplo de policies básicas:

-- Tabela com author_id
CREATE TABLE posts (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  author_id uuid REFERENCES auth.users(id) NOT NULL DEFAULT auth.uid(),
  title text,
  body text,
  created_at timestamptz DEFAULT now()
);
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Cada user vê só os próprios posts
CREATE POLICY "select own" ON posts
  FOR SELECT USING (author_id = auth.uid());

-- Pode inserir posts seus
CREATE POLICY "insert own" ON posts
  FOR INSERT WITH CHECK (author_id = auth.uid());

-- Pode editar/deletar só próprios
CREATE POLICY "update own" ON posts
  FOR UPDATE USING (author_id = auth.uid());
CREATE POLICY "delete own" ON posts
  FOR DELETE USING (author_id = auth.uid());
sql

Aplica via aba Migrations ou SQL Editor do projeto. Use a aba RLS pra editar policies via UI.

Transações

Cada call de client.db.query() já roda em transação implícita (BEGIN + sets de JWT context + COMMIT). Pra múltiplas operações atômicas, mande tudo num DO $$ BEGIN ... END $$ OU separe em statements numa única string com ;.

Bypass RLS (emergência)

Se você precisa ver/consertar dados como admin (ex: perdeu acesso a conta de user, precisa migrar dados manualmente), use o toggle Bypass RLS em Configurações do projeto. Vale por 1h.

Importante: o bypass aplica APENAS no painel (SQL Editor, Tables) e na CLI (admin). O SDK do app continua respeitando RLS — end-users não veem dados além das policies.

Limites

  • Pool: até 20 conexões simultâneas por projeto
  • Query timeout: 5 minutos (longo, mas pra evitar pendurar conexão)
  • Tamanho da query SQL: max 50.000 caracteres

Storage

Upload/download de arquivos via MinIO (S3-compat)

Cada projeto tem um bucket MinIO próprio com credenciais isoladas. SDK oferece tanto helpers (upload/download) quanto presigned URLs pra upload direto do browser sem passar bytes pelo InfraHub.

Upload + download (helpers de alto nível)

// Upload (arquivo do browser ou Buffer no Node)
const file = formInput.files[0]
const { error } = await client.storage.upload('avatars/user-42.png', file, {
  contentType: 'image/png',
})

// Download → Blob
const { data, error: dlErr } = await client.storage.download('avatars/user-42.png')
const url = URL.createObjectURL(data!)
ts

Presigned URLs (upload direto do browser)

Pra arquivos grandes (vídeo, dump), evite proxar bytes pelo InfraHub. Pega URL presigned, browser faz PUT direto no MinIO.

// 1. Pede URL presigned (TTL default 1h, max 24h)
const { url, method } = await client.storage.getUploadUrl('videos/clip.mp4', {
  expiresIn: 3600,
})

// 2. Browser faz PUT direto
await fetch(url, {
  method,
  body: file,
  headers: { 'Content-Type': 'video/mp4' },
})

// Download URL similar:
const { url: dlUrl } = await client.storage.getDownloadUrl('videos/clip.mp4')
window.location.href = dlUrl
ts

Listar e deletar

// Lista objects de um prefix
const { objects } = await client.storage.list({ prefix: 'avatars/', limit: 100 })
objects.forEach((o) => console.log(o.key, o.size))

// Deleta
await client.storage.delete('avatars/user-42.png')
ts

Bucket policies (público vs privado)

Por default, bucket é privado — só presigned URLs (com expiração) permitem acesso. Pra expor diretório como público (ex: avatares com URLs estáveis), use a aba Arquivos → Policy do projeto ou direto via SDK:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::meu-projeto/public/*"
    }
  ]
}

URLs públicas ficam em https://<slug>.<STORAGE_PUBLIC_DOMAIN>/<bucket>/<path>

Limites

  • Tamanho de arquivo via SDK helper: ~25 MB recomendado (proxa pelo Next). Acima disso, use presigned URL.
  • Presigned URL TTL: 60s a 24h
  • Quota total de storage: por plano (ver Limites & Quotas)

Realtime

Subscribe a INSERT/UPDATE/DELETE em tabelas via SSE

Realtime usa Server-Sent Events (SSE), não WebSocket. Funciona via HTTPS direto (sem custom server), reconexão automática do EventSource. Backend anexa um trigger Postgres (infraforge_realtime) na tabela e usa pg_notify pra streamar mudanças.

1. Ativar realtime numa tabela

No painel do projeto, aba Realtime, encontra a tabela e clica Ativar. Cria o trigger AFTER INSERT/UPDATE/DELETE chamando realtime.notify_change().

2. Subscribe no SDK

// Inscreve em mudanças da tabela 'posts' (schema 'public')
const sub = client.realtime.subscribe('posts', (change) => {
  console.log(change.op, change.new ?? change.old)
  // change.op: 'INSERT' | 'UPDATE' | 'DELETE'
  // change.new: row inserida/atualizada (null pra DELETE)
  // change.old: row antes do UPDATE/DELETE (null pra INSERT)
})

// Filtra por operação:
client.realtime.subscribe('posts', cb, { event: 'INSERT' })
client.realtime.subscribe('posts', cb, { event: ['INSERT', 'UPDATE'] })

// Outro schema (default 'public'):
client.realtime.subscribe('orders', cb, { schema: 'analytics' })

// Cancelar
sub.unsubscribe()
ts

Como a auth do SSE funciona

Browser EventSource não permite custom headers, então credenciais não vão direto na URL (era um bug de segurança CWE-598). Em vez disso:

  1. SDK faz POST /api/realtime/[slug]/session com headers normais → recebe sse_token efêmero (1h)
  2. Abre EventSource com ?sse_token=...
  3. Token tem purpose:'sse' — se vazar em log de proxy, só dá pra subscribe a esse endpoint, nada mais.

SDKs server-side (Python, Go, Java) mandam headers direto, sem precisar do handshake.

Limites

  • 16 canais por conexão SSE
  • 20 conexões SSE simultâneas por projeto (cada uma consome 1 client do pool PG)
  • ~8KB de payload por evento (pg_notify limite). Rows maiores chegam com truncated:true — cliente busca via client.db.query()
  • sse_token expira em 1h. Reconexão automática reaproveita o mesmo token; após 1h, faz nova handshake

CLI (`@infraforge/cli`)

Login, link, gen types, sql, migrations

CLI oficial pra desenvolvedor: introspecção do schema pra gerar tipos TypeScript, executar SQL admin, rodar migrations versionadas em git.

Instalação e setup

npm install -g @infraforge/cli

# Salva URL + API key globalmente em ~/.infraforge/config.json (chmod 600)
infraforge login

# Linka o diretório atual a um projeto (.infraforge/config.json no cwd)
infraforge link meu-projeto

Override por env: INFRAFORGE_URL, INFRAFORGE_API_KEY, INFRAFORGE_PROJECT. Override por flag: --url, --api-key, --project.

Type generation (Database TypeScript types)

infraforge gen types -o src/database.ts

Lê o schema do projeto (tabelas, colunas com tipos PG, PKs, FKs, enums) e gera interface TypeScript no estilo Supabase:

import type { Database, Tables, TablesInsert } from './database'

type Post = Tables<'public', 'posts'>
type NewPost = TablesInsert<'public', 'posts'>

const newPost: NewPost = {
  title: 'Hello',
  // colunas com default são opcionais
}
ts

Mapeia 30+ tipos PG (incluindo arrays _int4 e enums custom como union types).

Executar SQL admin

# Inline
infraforge sql "SELECT count(*) FROM posts"

# Arquivo
infraforge sql -f schema.sql

# Stdin
echo "TRUNCATE old_data" | infraforge sql

# JSON output
infraforge sql "SELECT * FROM posts" --json

Modo admin: bypass RLS automático. Usa apenas a API key (sem JWT). Quem tem a API key tem acesso total ao projeto via CLI.

Migrations versionadas em git

# Cria infraforge/migrations/<timestamp>_<nome>.sql
infraforge migrations new add_posts_table

# Aplica migrations pendentes em ordem alfabética
infraforge migrations up

Estado é mantido na tabela _infraforge_migrations do banco do projeto (id, applied_at). Re-rodar pula aplicadas.

Painel vs CLI: o painel usa um sistema separado (tabela migrations no meta-DB). Hoje os dois não se sincronizam. Migrations aplicadas via CLI não aparecem no painel e vice-versa. Item §6.4 do roadmap propõe unificar.

BYO Database (banco próprio)

Conectar Postgres existente em vez de usar managed_local

Se você já tem Postgres rodando (na sua VPS, RDS, Neon, Supabase, Hetzner Managed DB, etc.), pode conectar ao InfraHub em vez de deixar a plataforma spawnar um container novo. Vai em Meus bancos Novo banco.

Pré-requisitos importantes:

  • O servidor InfraHub precisa alcançar a porta do seu PG (geralmente 5432). Se firewall fechado, libere o IP de saída do InfraHub — está visível no card no topo de Meus bancos.
  • Usuário PG da connection URL precisa de CREATE SCHEMA / CREATE TABLE / CREATE FUNCTION no banco destino. Crie um user dedicado.
  • O InfraHub cria schemas auth (tabela auth.users + funções helper) e realtime. Se já tem com estrutura diferente, pode dar conflito.

Tutorial completo (firewall por sistema, criar user PG dedicado, diagnosticar conflitos com SQL pronto, soluções por cenário):

Após cadastrar o banco, ao criar projeto novo escolha modo external e selecione esse banco. Backups (pg_dump), migrations, SQL editor, RLS, type gen via CLI e Realtime passam a operar contra ele. Hospedagem da app continua na sua infra — InfraHub não hospeda apps ainda.

Migrations

Como gerenciar evolução do schema

Existem 2 caminhos pra aplicar migrations no banco do projeto. Eles fazem a mesma coisa (executar SQL) mas usam tabelas de controle diferentes — importante saber o trade-off.

Painel — aba Migrations

Cola SQL no textarea, dá nome, click Aplicar:

  • Grava em migrations (tabela no meta-DB do InfraHub)
  • Histórico fica visível no painel
  • Sem versionamento de arquivo, sem idempotência (re-rodar mesmo SQL falha 2× em CREATE TABLE)
  • Sem “down” — só forward. Pra desfazer, escreve outra migration manual
  • Bom pra: hotfix rápido, mudança one-off, exploração

CLI — `infraforge migrations`

# Cria infraforge/migrations/<timestamp>_<nome>.sql
infraforge migrations new add_orders_table

# Aplica pendentes em ordem alfabética (idempotente)
infraforge migrations up
  • .sql de infraforge/migrations/ (cwd)
  • Estado em _infraforge_migrations no banco do projeto
  • Idempotente — re-rodar pula migrations já aplicadas
  • Versionável em git → revisão de PR
  • Bom pra: produção, time, fluxo CI/CD

⚠ Gap entre os dois

Painel e CLI usam tabelas de controle diferentese não se sincronizam. Se você aplicar migration via CLI, o painel mostra “0 migrations”. Vice-versa idem.

Item §6.4 do roadmap propõe unificar (ambos gravando em _infraforge_migrations no banco do projeto). Por enquanto, escolha um caminho e fique nele.

Boas práticas

  • Use CREATE TABLE IF NOT EXISTS e ALTER TABLE ... ADD COLUMN IF NOT EXISTS pra garantir idempotência manual mesmo no painel
  • Prefira CLI pra qualquer projeto que vá pra produção
  • Evite queries enormes numa migration só — divide em arquivos
  • Backup antes de migration que toca dados (não só DDL)
  • Teste em projeto staging antes (BYO DB com banco separado pra dev)

Segurança

API key vs JWT, RLS, OAuth state, sessão

API key vs JWT — escopo de cada um

O InfraHub tem 2 tipos de credencial. Saber a diferença evita acidentes.

  • API key (if_xxx...): identifica o projeto. Acompanha toda request server-side via header x-infraforge-key. Vai no app cliente também (browser SDK precisa). Não é segredo absoluto — mas trate como secreta-ish (não cole em commit público de exemplo).
  • JWT do end-user: assinado com jwtSecret do projeto, identifica QUAL user fez a request. Fica no header Authorization: Bearer <jwt>. Backend valida em cada call e injeta como contexto pra RLS.
  • JWT secret: assina os JWTs. Nunca expor em código cliente. Só fica no InfraHub e no backend do app que precisar emitir tokens externos.

RLS — primeira linha de defesa

Sempre habilite RLS em qualquer tabela com dados por usuário:

ALTER TABLE minha_tabela ENABLE ROW LEVEL SECURITY;

-- Sem POLICY, ninguém vê nada (negação por default).
-- Adicione policies USING / WITH CHECK explícitas:
CREATE POLICY "select_own" ON minha_tabela
  FOR SELECT USING (owner_id = auth.uid());
sql

Tabela sem RLS = tudo público pra qualquer user autenticado. Tem aba RLS no painel pra auditar quais tabelas estão protegidas.

Bypass RLS — só pelo painel/CLI

Toggle Bypass RLS em Configurações dura 1h e libera SQL Editor + Tables + CLI a ignorar policies. SDK do app continua respeitando RLS — end-users não veem dados além das policies, mesmo com bypass ligado.

OAuth — proteção de session fixation

O fluxo OAuth do SDK gera um state random salvo em sessionStorage antes de redirecionar pro provider. Backend roundtripa o state via state assinado HMAC (TTL 10min). No callback, SDK compara — se mismatch, rejeita. Bloqueia ataque de atacante forjar link #token=ATTACKER_JWT pra fixar sessão na vítima.

Sessão no browser — escolha do storage

JWT da sessão é persistido em localStorage por default. Pra apps com dados muito sensíveis, escolha:

createClient({
  url, projectSlug, apiKey,
  session: 'localStorage',     // (default) persiste entre tabs
  // session: 'sessionStorage',// só durante a aba aberta
  // session: 'memory',        // nada gravado, refresh = re-login
})
  • localStorage: melhor UX, vulnerável a XSS de leitura no app cliente
  • sessionStorage: blast radius menor (limpa quando fecha aba)
  • memory: imune a XSS de leitura, mas usuário precisa logar de novo a cada refresh

Realtime — token efêmero

SSE auth usa sse_token curto-vivo (1h, purpose:'sse') em vez de credencial de longa duração na URL. Se vazar em log, expira rápido e só dá pra subscribe a esse endpoint do projeto, nada mais.

Hardening que está NA SUA mão (no app cliente)

  • Sanitizar input — XSS é bug do APP, não do framework
  • CSP restrita (Content-Security-Policy)
  • HTTPS obrigatório (HSTS — InfraHub já manda esse header)
  • Rate limit no seu lado se relevante (InfraHub tem por IP global, mas você pode ter cota por feature)
  • Não logue JWT em error tracker (Sentry, Datadog) — adicione filter

Limites & quotas

O que cada plano oferece

Quotas concretas estão na página Plano e uso → Planos (sempre atualizada). Resumo dos limites técnicos da plataforma (independente de plano):

RecursoLimiteOnde
Pool de conexões PG por projeto20MAX_CONNECTIONS_PER_PROJECT
Query timeout5 minQUERY_TIMEOUT_MS
Tamanho de query SQL50.000 chars/api/query schema
SQL admin (CLI/migrations)500.000 chars/api/admin/sql
Conexões SSE simultâneas~20 / projetolimitado pelo pool PG
Canais por conexão SSE16MAX_CHANNELS
Payload pg_notify (Realtime)~8 KBPostgres limit
Presigned URL TTL60s a 24hopção do upload-url/download-url
Backup retention (storage S3)por planogetBackupRetentionDays()
RLS bypass TTL60 minauto-desliga
OAuth state HMAC TTL10 minlink OAuth válido por isso

Limites por plano (projetos, BYO databases, storage GB, backup minutes/mês) são configuráveis e vivem em src/lib/plans.ts. Veja os valores reais na página de Planos.

SDKs por linguagem

Instalação e features por SDK

LinguagemPackageInstalarFeatures
TypeScript / JavaScript@infraforge/sdknpm install @infraforge/sdkAuth, DB, Storage, Realtime, OAuth com state validation, multi storage backend
Pythoninfraforgepip install infraforgeAuth, DB, Storage, Realtime via SSE (headers nativos)
Gogithub.com/seu-org/infraforge-gogo get github.com/seu-org/infraforge-goAuth, DB, Storage (Realtime planejado)
PHPinfraforge/sdkcomposer require infraforge/sdkAuth, DB, Storage
Rubyinfraforgegem install infraforgeAuth, DB, Storage
Javacom.infraforge:sdkMaven / GradleAuth, DB, Storage
C# / .NETInfraForge.Sdkdotnet add package InfraForge.SdkAuth, DB, Storage

SDK TypeScript é o mais completo (Realtime com handshake de SSE token, OAuth state validation, multi storage backend). Outros SDKs cobrem o essencial (auth + DB + storage). Realtime nos SDKs server-side usa headers nativos — sem necessidade do handshake do browser.

Versões e changelogs vivem nos READMEs dos pacotes. Pra acompanhar bumps, seguir o repo do InfraHub.

Troubleshooting / FAQ

Erros comuns e como resolver

Erro "connect ECONNREFUSED" ao cadastrar BYO Database

Firewall do seu Postgres está bloqueando o IP do InfraHub. Libere o IP de saída visível no card azul de Meus bancos.

Veja a seção BYO Database pra setups de firewall por sistema (UFW, pg_hba.conf, painéis cloud).

Erro "auth.users já existe" ao conectar BYO Database

Seu banco já tem schema auth de outro projeto/framework. Soluções:

  • Renomear: ALTER SCHEMA auth RENAME TO meu_auth
  • Criar banco separado pro InfraHub no mesmo PG (recomendado)

Veja BYO Database pra detalhes + SQL pronto pra diagnose.

RLS bloqueando query inesperadamente (0 rows quando deveria ter)

Default do PG: tabela com RLS ON e sem policy = bloqueia tudo (negação por default). Provável causa:

  • Tabela tem ENABLE ROW LEVEL SECURITY mas sem POLICY
  • POLICY existe mas a condição não bate (ex: auth.uid() retornando NULL porque JWT não tem sub)
  • User tem role diferente do esperado pela policy

Diagnose: ative bypass RLS (Configurações → 1h), rode a query — se voltar dados, é policy. Pra debug fino, no SQL editor:

-- Lista policies da tabela
SELECT * FROM pg_policies WHERE tablename = 'sua_tabela';

-- Confere que JWT context tá injetado
SELECT current_setting('request.jwt.sub', true), auth.uid();
sql
504 em query longa (export, backup, migration grande)

Backend do InfraHub aceita queries até 5 min. Se ainda bate timeout, provavelmente é o reverse proxy (Traefik/nginx) com timeout menor.

Adicione no docker-compose.yaml do InfraHub:

labels:
  - 'traefik.http.middlewares.app-timeout.buffering.responseTimeout=300s'
  - 'traefik.http.routers.app.middlewares=app-timeout'
EventSource fica reconectando infinitamente (Realtime)

Causas comuns:

  • sse_token expirou (TTL 1h). SDK reconecta com mesmo token → 401 → loop. Workaround: sub.unsubscribe() + novo subscribe() renova handshake.
  • Pool PG do projeto saturado (20 conexões). Fecha alguma SSE não usada ou aumenta o pool.
  • Tabela do canal sem trigger realtime ativo. Confirma na aba Realtime do projeto.
CLI "JWT inválido ou expirado"

CLI usa só API key, não JWT. Esse erro vem se você chamou infraforge sql sem login (rode infraforge login primeiro) ou se a API key salva está errada/revogada.

Confere com: cat ~/.infraforge/config.json

Browser reclama que URL do storage tem hostname Docker interno

O projeto não migrou pro endpoint público ainda. Vai em Configuraçõesdo projeto → “Expor storage publicamente” → click. ~30s de downtime do MinIO. Próximas presigned URLs já vêm com o subdomínio correto.

Pré-requisito: env STORAGE_PUBLIC_DOMAIN setado no servidor.

Form de cadastro de banco vem auto-preenchido com email/senha

Já corrigido na versão atual (autoComplete=“off” + new-password nos campos sensíveis). Se ainda acontece, pode ser cache do browser — recarrega com Ctrl+Shift+R.