# Isolamento Multi-Tenant ## Mecanismo Tabelas tenant-scoped usam PostgreSQL Row Level Security (RLS). A aplicação define, dentro de cada transação, o tenant atual via: ```sql SELECT set_config('app.current_tenant_id', '', true) ``` O terceiro argumento (`true` = `is_local`) faz o valor durar só a transação atual — seguro sob connection pooling, já que a mesma conexão física é reaproveitada por requisições de tenants diferentes. Toda tabela tenant-scoped repete o mesmo padrão de policy: ```sql ALTER TABLE "" ENABLE ROW LEVEL SECURITY; ALTER TABLE "" FORCE ROW LEVEL SECURITY; CREATE POLICY "tenant_isolation" ON "" USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid); ``` Sem `app.current_tenant_id` definido, `current_setting(..., true)` retorna `NULL`, a comparação vira `NULL`/falsa, e a query não retorna nenhuma linha — **deny-by-default**. Em código (`packages/database`), use sempre o helper `withTenantContext`: ```ts import { getPrismaClient, withTenantContext } from "@b2bcall/database"; const prisma = getPrismaClient(); const memberships = await withTenantContext(prisma, tenantId, (tx) => tx.tenantMembership.findMany(), ); ``` Nunca faça `prisma.tenantMembership.findMany()` direto (fora de `withTenantContext`) em código de aplicação — isso corre em cima da conexão sem contexto de tenant e, com a policy acima, simplesmente não retorna nada (falha "segura", mas ainda assim é um bug: use sempre o helper). ## Achado crítico: role de conexão importa mais que a policy A imagem oficial do Postgres no Docker sempre cria o usuário inicial (`POSTGRES_USER`, aqui `b2bcall`) como **SUPERUSER**. Superusers — e qualquer role com o atributo `BYPASSRLS` — **sempre ignoram RLS**, mesmo com `FORCE ROW LEVEL SECURITY` habilitado. Não existe exceção via configuração de policy: é uma regra do Postgres anterior à avaliação de qualquer `USING`/`WITH CHECK`. Por isso o projeto usa **dois roles de banco diferentes**: | Role | Uso | Privilégios | Variável de ambiente | |----------------|----------------------------------------|----------------------------------|-----------------------| | `b2bcall` | Migrations, DDL, dono das tabelas | SUPERUSER (padrão da imagem) | `DATABASE_URL` | | `b2bcall_app` | Runtime da aplicação (API, workers) | Sem SUPERUSER, sem BYPASSRLS | `APP_DATABASE_URL` | `getPrismaClient()` em `packages/database/src/index.ts` só aceita `APP_DATABASE_URL` e lança erro se não estiver definida — propositalmente, para impedir que algum serviço se conecte por engano como o role superuser e sile nciosamente ignore o isolamento entre tenants. O role `b2bcall_app` é criado pela migration `app_role_and_grants` (sem senha — senha nunca fica em migration versionada no Git). A senha é aplicada via `scripts/db-setup-app-role.sh`, que lê `POSTGRES_APP_PASSWORD` do `.env` (fora do Git). ## Teste de verificação `packages/database/src/__tests__/tenant-isolation.test.ts` cria dois tenants e confirma que o contexto de um nunca enxerga dados do outro, e que sem contexto nenhuma linha tenant-scoped é visível. Rodar com: ```bash pnpm --filter @b2bcall/database run test:isolation ``` Isso corresponde ao critério de aceite da seção 204 do `agente.md` ("User Tenant A tentando acessar dado Tenant B: resultado obrigatório 403/404, nunca dado do Tenant B" — aqui verificado na camada de banco, a camada de API/HTTP ainda vai reforçar isso com 403/404 quando a Fase de Autenticação/RBAC existir). ## Ao criar uma nova tabela tenant-scoped 1. Adicionar `tenantId String @map("tenant_id") @db.Uuid` + relação com `Tenant`. 2. Na migration gerada, adicionar o bloco `ENABLE/FORCE ROW LEVEL SECURITY` + `CREATE POLICY "tenant_isolation" ...` (copiar o padrão acima). 3. Não é necessário adicionar `GRANT` manual para `b2bcall_app` — a migration `app_role_and_grants` já configurou `ALTER DEFAULT PRIVILEGES` cobrindo tabelas futuras criadas pelo role de migration. 4. Toda leitura/escrita nessa tabela em código de aplicação deve passar por `withTenantContext`.