O InfraForge é uma plataforma self-hosted que 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 headerx-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 InfraForge usa pra validar JWT em cada query. Nunca deve sair do InfraForge nem do servidor do app que precisar emitir tokens externos.
- RLS (Row-Level Security): políticas SQL que filtram linhas por usuário. InfraForge injeta
request.jwt.sub/role/emailem cada query do SDK, e suas policies referenciamauth.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
1. Criar projeto
No painel, vai em Projetos → Novo projeto. Escolhe modo:
- managed_local: InfraForge 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: InfraForge 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/sdkts
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!,
})ts4. 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 logadotsPróximo: as seções Autenticação, Database e Storage cobrem cada módulo em detalhe.
End-users do seu app são gerenciados pelo InfraForge: 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)tsModos 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)
}tsSeguranç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 InfraForge (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' }),
})tssetToken — 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.
Código server-side que roda sem um usuário interativo (worker em background, escritas de billing, migrations no boot de um container) não tem sessão pra chamar db.query(). Só com a API key, o SDK retorna Not authenticated — e o endpoint /api/query/[slug] exige os dois: o header x-infraforge-key (API key) e um Authorization: Bearer <JWT>. A API key sozinha nunca basta.
A solução é mintar um service token: um JWT que você assina no seu servidor com o JWT Secret do projeto. O InfraForge valida só a assinatura e a expiração — não exige que o token corresponda a um login real. Um único service token resolve worker, billing e migrations.
Mintando o service token
Assine um JWT HS256 com o JWT Secret do projeto e injete via auth.setToken(). A partir daí, db.query() manda API key + Bearer juntos:
import { SignJWT } from 'jose'
import { createClient } from '@infraforge/sdk'
// O JWT Secret do projeto — guarde como segredo de servidor (env var),
// NUNCA no client. Não é o NEXTAUTH_SECRET da plataforma.
const secret = new TextEncoder().encode(process.env.INFRAFORGE_JWT_SECRET!)
const serviceToken = await new SignJWT({
sub: '00000000-0000-0000-0000-000000000000', // UUID válido (auth.uid() faz ::uuid)
email: 'service@suaapp.local', // precisa ser NÃO-vazio
role: 'service_role', // role reconhecida pelo InfraForge
})
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('30d') // longo, mas finito — rotacione quando necessário
.sign(secret)
const client = createClient({
url: process.env.INFRAFORGE_URL!,
projectSlug: 'meu-app',
apiKey: process.env.INFRAFORGE_API_KEY!,
})
client.auth.setToken(serviceToken)
// Agora roda server-side, sem usuário interativo:
const { data, error } = await client.db.query('SELECT * FROM invoices')tsOnde achar o JWT Secret
No painel: Projeto → aba Visão geral → Credenciais → “JWT Secret”. É o mesmo segredo que o InfraForge usa pra assinar os tokens de login dos seus end-users.
⚠ É um segredo de servidor: quem tem ele pode forjar qualquer identidade no seu projeto. Mantenha em env var, nunca commite, nunca exponha no bundle do client. Não confunda com o NEXTAUTH_SECRET da plataforma — esse não vale aqui.
Gotchas das claims
sub,emaileroleprecisam ser não-vazios — o backend rejeita a query se algum vier vazio. (roleassumeauthenticatedpor default;emailnão tem default seguro.)subé convertido prauuidporauth.uid()— use um UUID válido (ex: o all-zeros) ou queries que chamamauth.uid()quebram no cast.
Migrations via SDK
Com um service token, db.query() roda DDL normalmente (a query proxy aceita SQL arbitrário e executa com privilégio de owner do banco). Dá pra rodar migrations no boot de um container:
client.auth.setToken(serviceToken)
await client.db.query(`
CREATE TABLE IF NOT EXISTS jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
`)tsPra múltiplos statements num único query(), não use parâmetros (params vazio): com $1 o Postgres entra no extended protocol e aceita só 1 statement por chamada. Pra fluxo versionado/idempotente, prefira o CLI infraforge migrations (ver seção Migrations).
RLS e o service token
A query proxy nunca faz bypass de RLS — nem com service token. O toggle de bypass do painel (TTL 60min) é só pra recuperação emergencial, não serve pra worker always-on. O que o token consegue depende das suas políticas:
- A conexão ao banco do projeto é como owner, e o Postgres só aplica RLS em tabelas marcadas com
FORCE ROW LEVEL SECURITY. SemFORCE, o service token (e qualquer end-user) ignora as políticas e enxerga tudo — confira isso se seu isolamento entre usuários depende de RLS. - Com
FORCE ROW LEVEL SECURITY, crie uma política que libere o service explicitamente:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; ALTER TABLE invoices FORCE ROW LEVEL SECURITY; -- End-users só veem as próprias linhas CREATE POLICY invoices_owner ON invoices USING (user_id = auth.uid()); -- O service token (role='service_role') acessa tudo CREATE POLICY invoices_service ON invoices USING (auth.role() = 'service_role');sql
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)tsRLS — 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 usersql
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
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 InfraForge.
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!)tsPresigned URLs (upload direto do browser)
Pra arquivos grandes (vídeo, dump), evite proxar bytes pelo InfraForge. 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 = dlUrltsListar 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')tsBucket 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 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()tsComo 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:
- SDK faz
POST /api/realtime/[slug]/sessioncom headers normais → recebesse_tokenefêmero (1h) - Abre
EventSourcecom?sse_token=... - 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 viaclient.db.query() - sse_token expira em 1h. Reconexão automática reaproveita o mesmo token; após 1h, faz nova handshake
Gateway unificado de Telegram e WhatsApp. Canais (bot do Telegram, login de usuário Telegram, WhatsApp Cloud API, WhatsApp não oficial embutido, Evolution API, WAHA) são configurados no painel — aba Mensageria global ou dentro de cada projeto. Cada plataforma (telegram ou whatsapp) resolve pro canal com override específico do projeto, senão pro canal padrão do tenant.
Enviar mensagem
const msg = client.messaging()
await msg.send({ platform: 'telegram', to: chatId, text: 'Olá!' })
// Força um canal específico (ignora a resolução automática):
await msg.send({ platform: 'whatsapp', channel: 'suporte', to: numero, text: 'Seu pedido saiu pra entrega.' })
// Template do WhatsApp Cloud (obrigatório fora da janela de 24h):
await msg.send({
platform: 'whatsapp_cloud',
to: numero,
template: { name: 'pedido_confirmado', params: { nome: 'Maria' }, language: 'pt_BR' },
})
// Evita duplicata em retry:
await msg.send({ platform: 'telegram', to: chatId, text: 'Confirmado', idempotencyKey: pedidoId })tsReceber mensagens em tempo real (SDK TypeScript)
Mesmo padrão do client.realtime.subscribe(), via SSE, autenticado só pela API key do projeto (mensageria é uma capacidade de projeto, não por usuário final).
const msg = client.messaging()
const sub = msg.on('message', (envelope) => {
console.log(envelope.platform, envelope.text, envelope.senderId)
// envelope.contentType: 'text' | 'image' | 'document' | 'video' | 'audio' | 'button' | 'location' | 'other'
})
// depois...
sub.unsubscribe()tsReceber via webhook (alternativa pra qualquer linguagem)
Configure um Webhook URL nas configurações de Mensageria do projeto (painel). Toda mensagem de entrada vira um POST assinado por HMAC-SHA256, sem precisar do SDK:
- Header
x-infraforge-signature: sha256=<hmac>do corpo cru, calculado com o segredo do webhook configurado (ou o JWT secret do projeto, se nenhum segredo específico for definido) - Corpo:
{ platform, channelKind, conversationId, senderId, contentType, text, attachments, timestamp } - Retry automático com backoff se seu endpoint responder erro ou timeout (10s)
Assistente de IA por projeto
Cada projeto pode ligar um assistente que responde automaticamente mensagens de texto recebidas, usando um alias do módulo de IA já configurado, com prompt de sistema e histórico de conversa próprios do projeto — configurado inteiramente pelo painel(aba do projeto → Mensageria → Assistente), sem chamada nenhuma de SDK. Um toggle opcional "simular comportamento humano" adiciona atraso de leitura/digitação e quebra a resposta em mensagens curtas, reduzindo (não eliminando) o risco de banimento em canais que emulam o WhatsApp Web (não oficial, Evolution, WAHA).
Disponibilidade por linguagem
O cliente messaging() (envio + on('message')) existe hoje só no SDK TypeScript. As outras linguagens (Python, Go, PHP, Ruby, Java, C#) ainda não têm esse wrapper — pra usar mensageria a partir delas, chame a API REST direto (POST /api/messaging/send, exemplo cURL acima) e/ou configure o webhook, que funciona com qualquer linguagem sem precisar de SDK nenhum.
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
}tsMapeia 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.
Se você já tem Postgres rodando (na sua VPS, RDS, Neon, Supabase, Hetzner Managed DB, etc.), pode conectar ao InfraForge em vez de deixar a plataforma spawnar um container novo. Vai em Meus bancos → Novo banco.
Pré-requisitos importantes:
- O servidor InfraForge precisa alcançar a porta do seu PG (geralmente 5432). Se firewall fechado, libere o IP de saída do InfraForge — está visível no card no topo de Meus bancos.
- Usuário PG da connection URL precisa de
CREATE SCHEMA/CREATE TABLE/CREATE FUNCTIONno banco destino. Crie um user dedicado. - O InfraForge cria schemas
auth(tabelaauth.users+ funções helper) erealtime. 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 — InfraForge não hospeda apps ainda.
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 InfraForge) - 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ê
.sqldeinfraforge/migrations/(cwd) - Estado em
_infraforge_migrationsno 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 diferentes e 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 EXISTSeALTER TABLE ... ADD COLUMN IF NOT EXISTSpra 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)
API key vs JWT — escopo de cada um
O InfraForge tem 2 tipos de credencial. Saber a diferença evita acidentes.
- API key (
if_xxx...): identifica o projeto. Acompanha toda request server-side via headerx-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
jwtSecretdo projeto, identifica QUAL user fez a request. Fica no headerAuthorization: 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 InfraForge 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 clientesessionStorage: 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 — InfraForge já manda esse header)
- Rate limit no seu lado se relevante (InfraForge tem por IP global, mas você pode ter cota por feature)
- Não logue JWT em error tracker (Sentry, Datadog) — adicione filter
Quotas concretas estão na página Plano e uso → Planos (sempre atualizada). Resumo dos limites técnicos da plataforma (independente de plano):
| Recurso | Limite | Onde |
|---|---|---|
| Pool de conexões PG por projeto | 20 | MAX_CONNECTIONS_PER_PROJECT |
| Query timeout | 5 min | QUERY_TIMEOUT_MS |
| Tamanho de query SQL | 50.000 chars | /api/query schema |
| SQL admin (CLI/migrations) | 500.000 chars | /api/admin/sql |
| Conexões SSE simultâneas | ~20 / projeto | limitado pelo pool PG |
| Canais por conexão SSE | 16 | MAX_CHANNELS |
| Payload pg_notify (Realtime) | ~8 KB | Postgres limit |
| Presigned URL TTL | 60s a 24h | opção do upload-url/download-url |
| Backup retention (storage S3) | por plano | getBackupRetentionDays() |
| RLS bypass TTL | 60 min | auto-desliga |
| OAuth state HMAC TTL | 10 min | link 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.
| Linguagem | Package | Instalar | Features |
|---|---|---|---|
| TypeScript / JavaScript | @infraforge/sdk | npm install @infraforge/sdk | Auth, DB, Storage, Realtime, OAuth com state validation, multi storage backend |
| Python | infraforge | pip install infraforge | Auth, DB, Storage, Realtime via SSE (headers nativos) |
| Go | github.com/seu-org/infraforge-go | go get github.com/seu-org/infraforge-go | Auth, DB, Storage (Realtime planejado) |
| PHP | infraforge/sdk | composer require infraforge/sdk | Auth, DB, Storage |
| Ruby | infraforge | gem install infraforge | Auth, DB, Storage |
| Java | com.infraforge:sdk | Maven / Gradle | Auth, DB, Storage |
| C# / .NET | InfraForge.Sdk | dotnet add package InfraForge.Sdk | Auth, 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 InfraForge.
Erro "connect ECONNREFUSED" ao cadastrar BYO Database▾
Firewall do seu Postgres está bloqueando o IP do InfraForge. 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 InfraForge 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 SECURITYmas sem POLICY - POLICY existe mas a condição não bate (ex:
auth.uid()retornando NULL porque JWT não temsub) - 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();sql504 em query longa (export, backup, migration grande)▾
Backend do InfraForge aceita queries até 5 min. Se ainda bate timeout, provavelmente é o reverse proxy (Coolify/Traefik) com timeout menor.
Adicione no docker-compose.yaml do InfraForge:
labels: - 'traefik.http.middlewares.app-timeout.buffering.responseTimeout=300s' - 'traefik.http.routers.app.middlewares=app-timeout'
EventSource fica reconectando infinitamente (Realtime)▾
Causas comuns:
sse_tokenexpirou (TTL 1h). SDK reconecta com mesmo token → 401 → loop. Workaround:sub.unsubscribe()+ novosubscribe()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ções do 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.