Toda app no theo tem um banco PostgreSQL próprio. Você acessa via db (passado como prop pro componente do app) ou Bea.db (alias global).
Como funciona
Criar tabelas
useEffect(() => {
db.execute(`
CREATE TABLE IF NOT EXISTS app_clientes_x7m2 (
id SERIAL PRIMARY KEY,
owner_id TEXT NOT NULL,
nome TEXT NOT NULL,
telefone TEXT,
created_at TIMESTAMPTZ DEFAULT now()
)
`);
db.execute(`CREATE INDEX IF NOT EXISTS idx_clientes_owner_x7m2 ON app_clientes_x7m2(owner_id)`);
}, []);
Nomes seguem app_<entidade>_<hash> onde hash é único do app (4-5 chars). A IA gera automaticamente. Nunca compartilhe tabelas entre apps sem usar App Connections.
SEMPRE inclua owner_id TEXT NOT NULL em toda tabela e filtre nas queries — RLS no banco depende disso. Sem owner_id, dados vazam entre usuários.
Ler dados
const rows = await db.query(
"SELECT * FROM app_clientes_x7m2 WHERE owner_id = $1 ORDER BY nome",
[user.id]
);
setClientes(rows);
Com busca (sempre parametrize, nunca concatene):
await db.query(
"SELECT * FROM app_clientes_x7m2 WHERE owner_id = $1 AND nome ILIKE $2",
[user.id, `%${busca}%`]
);
Nunca use template literal ${nome} direto na query. Use $1, $2, ... e passe valores no array. SQL injection é problema real mesmo no app gerado por IA.
Inserir / atualizar / deletar
// Insert
await db.execute(
"INSERT INTO app_clientes_x7m2 (owner_id, nome, telefone) VALUES ($1, $2, $3)",
[user.id, nome, tel]
);
// Update — sempre filtra por owner_id pra não mexer em dados de outros
await db.execute(
"UPDATE app_clientes_x7m2 SET telefone = $1 WHERE id = $2 AND owner_id = $3",
[novoTel, id, user.id]
);
// Delete
await db.execute(
"DELETE FROM app_clientes_x7m2 WHERE id = $1 AND owner_id = $2",
[id, user.id]
);
Diferença: query vs execute
| Função | Retorna | Use pra |
|---|---|---|
db.query(sql, params) | Array de objetos [{...}, {...}] | SELECT, COUNT, retornos genéricos |
db.execute(sql, params) | Apenas { ok: true } | INSERT, UPDATE, DELETE, CREATE, ALTER |
Mesmo INSERT ... RETURNING * use com query (porque você quer o resultado).
Limites
- Timeout de 10s por query
- 240 requests/min por app (proteção contra loop) — se ultrapassar, banner aparece pedindo pra revisar useEffect
- Apenas tabelas do próprio app — cross-app só via App Connections
- Comandos bloqueados:
DROP TABLEem tabela de outro app,TRUNCATE,GRANT, qualquer DDL fora do schema do app
Se você ver banner "Loop detectado: app excedeu 240 queries/min", causa típica é useEffect com db.query que dispara setState sem dependency array correto, OU setInterval sem cleanup. Verifique nesses dois lugares.
Debug — DB Explorer
Admin pode acessar Sidebar›Admin›Database pra ver tabelas e dados de qualquer app. Útil pra:
- Confirmar que dados estão chegando
- Ver schema real (vs. o que o LLM gerou)
- Limpar dados de teste
Próximo: Bea.* helpers úteis →
Tabelas compartilhadas (sistemas modulares)
Em sistemas modulares, sub-apps acessam tabelas declaradas no app pai via prefixo app_<parent-slug>_shared_<entity>. Acesso é read + write entre toda a família.
// Sub-app "Pipeline" lendo + escrevendo na tabela compartilhada
const clientes = await db.query("SELECT * FROM app_crm_shared_clientes ORDER BY nome");
await db.execute("INSERT INTO app_crm_shared_oportunidades (cliente_id, valor) VALUES ($1, $2)", [clienteId, valor]);
Declarado em /app/<pai>/data-model. Veja Modelo de dados.
Cross-app reads (siblings)
Apps no mesmo negócio (não modular) podem fazer SELECT uns nos outros (read-only):
// App "Faturas" lendo tabela do app "Clientes" (mesmo business)
const clientes = await db.query("SELECT * FROM app_clientes_x7k2");
Diferença: shared tables (modular) = read+write. Sibling tables (business) = read-only.