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...
4

Risco de vazamento Médio sem RLS, baixo com RLS

Aqui eu preciso discordar.

Risco de vazamento é Alto, médio com RLS.

Se você, ou qualquer outro dev, esquecer de ligar o RLS em uma única tabela pode ocorrer o vazamento

O risco é muito maior que isolar schemas, que o que você tem que lembrar é basicamente gerenciar a conexão.

RLS pode ser seguro, mas ainda depende dos devs lembrarem que ele existe

Limite prático de tenants Milhares Centenas Dezenas

Dezenas com base em que? com dinheiro infinito eu tenho bases infinitas, inclusive database per tenant é o único que não tem limite

Entrou um cliente novo? só subir uma nova instância

-3

Esquecer o que? Um tenantId? Que empresa chibata é essa que não testa a porcaria do código? Pelo amor de deus. Dificultando coisa simples. Até parece que você criou o novo totvs

1
1

Meus 2 cents: Um dos motivos de eu não gostar muito de multitenancy com o prisma é porque para criar um sistema de multischema com ele fica bem complexo de não ter vazamento de dados, e também já estive em trabalhos aonde as queries ficavam muito complexas e lentas mesmo indexando, tem gente que acha que é só sair colocando o tenantId em todos os campus e isso já irá resolver, porem no momento que escala se a arquitetura do código não tiver tão completa acaba que facilitando na compreensão a custo de performance e outras coisas, pra esses cenários normalmente eu uso o typeorm como neste exemplo: https://github.com/JulioVianaDev/multitenancy-postgresl-nestjs e o typeorm me permite customizar um conector até se eu quiser separar os acessos via ssh em servidores diferentes, ou fazer um cqrs para alguns clientes que tem tenants maiores. O próprio jira usava a arquitetura de um database por tenant, até trocarem para o cassandra por decisões arquiteturais depois de escalarem.

Outra coisa com RLS é extremamente custoso tu deletar tudo de um cliente do banco de dados( apesar da LGPD ser um "fantasma" no brasil), eu tenho clientes que cobram a risca o cliente me pede para exportar todo o banco de dados deles em contrato com o DPO e isso acaba acarretando de isso pesar o servidor principal do banco de dados, nesses casos para os clientes de lgpd tenho um servidor separado que funciona como um banco de dados de backup, salvando os dados em sequencia ao principal, bem parecido com um cqrs que meu cliente pode ter acesso entre outras coisas.

Para finalizar o que eu faria seria começar com 1 schema por cliente, se o cliente crescer 1 banco de dados por cliente, e por fim até mesmo um servidor para um cliente especifico, mas isso é por causa de softwares mais enterprise que contém dados muito sensiveis, e vazamento no brasil é algo que quando tratado com seriedade pode chegar a um prejuizo enorme dependendo do processo, por exemplo se seu software é um software B2B2C e tem dados dos clientes dos seus usuários, isso causa uma multa por cliente afetado, detesto me preocupar com a parte juridica porém é importante.