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. O InfraHub spawna um container Postgres + MinIO isolado na infra da plataforma — sem setup.

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.

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
  • Lê .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 (um projeto 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

Os limites de recursos da sua conta

Seus limites de recursos (sites, projetos, storage) estão em Uso e limites. Quem define esses limites é o administrador ou gestor da sua organização. Resumo dos limites técnicos da plataforma (iguais pra todos):

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)30 dias (default)definido pela org (quota)
RLS bypass TTL60 minauto-desliga
OAuth state HMAC TTL10 minlink OAuth válido por isso

Os limites de recursos (projetos, storage GB) são definidos pela hierarquia da sua organização (admin/gestor) e aplicados via quota por usuário.

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

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.