6

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érioColuna tenantId (shared DB)Schema por tenantDatabase por tenant
Isolamento de dadosLógico (aplicação + RLS)Físico por schemaFísico total
Complexidade de migrationsBaixa (1 migration para todos)Média (N schemas para atualizar)Alta (N databases)
Custo de infraBaixoMédioAlto
Performance de queries cross-tenantSimples (WHERE)Requer qualified namesRequer conexões separadas
Risco de vazamentoMédio sem RLS, baixo com RLSBaixoMuito baixo
Limite prático de tenantsMilharesCentenasDezenas
Compatibilidade com PrismaNativaRequer troca de schema em runtimeRequer 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)
Carregando publicação patrocinada...
1