Multitenancy com Prisma e PostgreSQL: Row-Level Security vs Schema por Tenant
Multitenancy com Prisma e PostgreSQL: Row-Level Security vs Schema por Tenant
Um bug de multitenancy não retorna 500. Retorna os dados do cliente errado. É o tipo de falha que destrói confiança, gera processo e não aparece em nenhum dashboard de erro. Por isso a decisão de como isolar tenants merece mais atenção do que a maioria dos times dedica.
Este post implementa duas estratégias completas de multitenancy com Prisma e PostgreSQL: isolamento por coluna com Row-Level Security (RLS) e isolamento por schema. Código funcional, migrations reais e os pontos onde cada abordagem quebra.
As três estratégias e quando cada uma faz sentido
Antes de escrever código, a decisão de arquitetura define o teto de complexidade do projeto inteiro.
| Critério | Coluna tenantId (shared DB) | Schema por tenant | Database por tenant |
|---|---|---|---|
| Isolamento de dados | Lógico (aplicação + RLS) | Físico por schema | Físico total |
| Complexidade de migrations | Baixa (1 migration para todos) | Média (N schemas para atualizar) | Alta (N databases) |
| Custo de infra | Baixo | Médio | Alto |
| Performance de queries cross-tenant | Simples (WHERE) | Requer qualified names | Requer conexões separadas |
| Risco de vazamento | Médio sem RLS, baixo com RLS | Baixo | Muito baixo |
| Limite prático de tenants | Milhares | Centenas | Dezenas |
| Compatibilidade com Prisma | Nativa | Requer troca de schema em runtime | Requer troca de datasource |
Database por tenant é viável para menos de 20 clientes enterprise com requisitos regulatórios rígidos (LGPD com isolamento total, por exemplo). Para a maioria dos SaaS, a escolha real fica entre coluna com RLS e schema por tenant. Este post cobre as duas.
Estratégia 1: coluna tenantId com Row-Level Security
A abordagem mais comum. Cada tabela carrega uma coluna tenantId e o PostgreSQL garante, via RLS, que nenhuma query acesse dados de outro tenant, mesmo que a aplicação tenha um bug.
Schema do Prisma
// schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["multiSchema"] // necessário apenas na estratégia 2
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Tenant {
id String @id @default(cuid())
name String
slug String @unique
createdAt DateTime @default(now()) @map("created_at")
users User[]
projects Project[]
@@map("tenants")
}
model User {
id String @id @default(cuid())
email String
tenantId String @map("tenant_id")
tenant Tenant @relation(fields: [tenantId], references: [id])
// índice composto garante que email é único DENTRO do tenant
@@unique([tenantId, email])
@@map("users")
}
model Project {
id String @id @default(cuid())
name String
tenantId String @map("tenant_id")
tenant Tenant @relation(fields: [tenantId], references: [id])
@@index([tenantId])
@@map("projects")
}
Migration SQL para habilitar RLS
O Prisma não gera políticas RLS automaticamente. Você precisa de uma migration SQL manual. Execute com prisma migrate usando um arquivo SQL customizado ou via prisma db execute.
-- migrations/20240101_enable_rls.sql
-- RLS funciona com uma variável de sessão que a aplicação define antes de cada query
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Política: só retorna linhas cujo tenant_id bate com a variável de sessão
-- current_setting com 'true' no segundo argumento retorna string vazia se a variável não existir,
-- em vez de lançar erro (evita quebrar migrations e queries administrativas)
CREATE POLICY tenant_isolation_users ON users
USING (tenant_id = current_setting('app.current_tenant_id', true))
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true));
CREATE POLICY tenant_isolation_projects ON projects
USING (tenant_id = current_setting('app.current_tenant_id', true))
WITH CHECK (tenant_id = curren
---
Leia o artigo completo em [https://www.vivodecodigo.com.br/backend/multitenancy-prisma-postgresql-rls-schema-por-tenant](https://www.vivodecodigo.com.br/backend/multitenancy-prisma-postgresql-rls-schema-por-tenant)