Multi-tenancy no Postgres: Schema vs Row Level Security
O que é multi-tenancy e por que o modelo de dados importa
Multi-tenancy é o padrão em que múltiplos clientes (tenants) compartilham a mesma infraestrutura de aplicação e, na maioria dos casos, o mesmo banco de dados. Cada tenant enxerga apenas os próprios dados, mas todos rodam sobre o mesmo Postgres, o mesmo código de aplicação e, frequentemente, as mesmas tabelas.
O isolamento de dados é a métrica que mais importa nesse modelo. Um vazamento entre tenants — o cliente A vendo dados do cliente B — não é um bug qualquer: é o tipo de incidente que encerra contratos e, dependendo do setor, gera exposição legal. A escolha do modelo de dados (schema-per-tenant ou Row Level Security) define o quanto esse risco fica embutido na arquitetura versus dependente de disciplina de código.
Essa escolha também define o custo operacional do SaaS a médio prazo. Um modelo com isolamento forte tende a custar mais em manutenção de infraestrutura; um modelo com isolamento lógico tende a custar mais em disciplina de engenharia e testes. Não existe opção sem custo — existe opção com o custo em lugar diferente.
Este artigo assume que você já decidiu multi-tenant compartilhado em vez de banco dedicado por cliente. Se essa decisão ainda está em aberto, o Guia Definitivo de Arquitetura SaaS em 2026 cobre esse ponto anterior na árvore de decisão.
As três entidades centrais deste comparativo são o PostgreSQL como banco relacional, o schema-per-tenant como estratégia de isolamento físico via namespaces do banco, e o Row Level Security (RLS) como estratégia de isolamento lógico via políticas de filtro em nível de linha. Uma quarta entidade, connection pooling, entra na equação porque a forma como você gerencia conexões ao Postgres interage diretamente com qual dos dois modelos é viável em escala.
Schema-per-tenant: como funciona
No modelo schema-per-tenant, cada cliente recebe um schema Postgres próprio — tenant_acme, tenant_globex — com o mesmo conjunto de tabelas replicado em cada um. A aplicação seleciona o schema correto via SET search_path no início de cada conexão, geralmente resolvido a partir de um identificador de tenant no request.
O isolamento aqui é forte porque acontece a nível de banco. Uma query mal escrita no código da aplicação não tem como, por acidente, ler dados de outro schema — a conexão simplesmente não enxerga aquelas tabelas a menos que o search_path mude. Isso reduz drasticamente a superfície de erro humano que causaria vazamento entre tenants.
O trade-off aparece na operação. Uma migration de schema não roda uma vez — roda N vezes, uma por tenant. Ferramentas como Flyway ou Prisma Migrate precisam iterar sobre a lista de schemas, e uma migration que falha no meio do caminho deixa o banco em estado inconsistente entre tenants — alguns já migrados, outros não.
Existe também um limite prático de escala. O Postgres suporta um número grande de schemas por banco, mas cada schema replica catálogo de sistema, planos de query em cache e overhead de metadados. Em instalações com muitos milhares de schemas, é comum observar degradação em operações de catálogo (como \dt no psql ou introspecção via ORM) antes mesmo de qualquer gargalo de I/O.
Row Level Security: como funciona
Row Level Security é um recurso nativo do Postgres, documentado oficialmente, que permite definir políticas de filtro aplicadas automaticamente a toda query contra uma tabela — incluindo SELECT, UPDATE e DELETE. Em vez de um schema por tenant, todos os tenants compartilham as mesmas tabelas, e uma coluna tenant_id diferencia as linhas.
O fluxo típico é habilitar RLS na tabela e criar uma policy que compara tenant_id com um valor de contexto de sessão:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.current_tenant')::uuid);
A aplicação, antes de cada query, define o tenant da sessão:
SET app.current_tenant = '3fa85f64-5717-4562-b3fc-2c963f66afa6';
A partir daí, mesmo um SELECT * FROM invoices sem WHERE retorna apenas as linhas do tenant ativo. O Postgres aplica o filtro de forma transparente, no nível do planejador de query.
O ponto frágil desse modelo é que ele depende inteiramente de esse contexto de sessão estar correto. Se o código da aplicação esquecer de rodar o SET app.current_tenant antes de uma query — ou reutilizar uma conexão de pool sem resetar essa variável —, o comportamento pode variar de "nenhuma linha retornada" a, em configurações mal feitas com BYPASSRLS em roles administrativas, vazamento silencioso de dados de outro tenant. Uma policy mal escrita, como usar OR em vez de AND em condições compostas, é o tipo de erro que passa despercebido em testes e aparece só em produção com volume real de tenants.
Comparação direta entre os dois modelos
| Critério | Schema-per-tenant | Row Level Security |
|---|---|---|
| Isolamento de dados | Forte, a nível de banco | Lógico, depende de policy correta |
| Complexidade de migration | Alta — roda em N schemas | Baixa — roda uma vez, tabela única |
| Performance em muitos tenants | Degrada com milhares de schemas | Escala melhor, mas exige índice em tenant_id |
| Compatibilidade com connection pooling agressivo | Baixa — search_path por conexão |
Alta — variável de sessão simples |
| Custo de manutenção operacional | Alto (backups, migrations, monitoramento por schema) | Baixo, mas exige disciplina de código e testes de policy |
| Facilidade de dar a um cliente enterprise um "banco isolado" | Natural | Requer schema dedicado à parte |
Schema-per-tenant compensa quando o número de tenants é baixo e cada um é grande — dezenas ou poucas centenas de clientes enterprise, cada um com volume de dados relevante e exigências contratuais de isolamento físico auditável.
RLS compensa no cenário oposto: muitos tenants pequenos, típico de SaaS self-service com centenas ou milhares de contas, onde rodar migration em cada schema individualmente se torna operacionalmente inviável e o pooling de conexões precisa ser agressivo para manter custo de infraestrutura baixo.
Impacto em connection pooling e performance
O connection pooling é onde a teoria dos dois modelos encontra a realidade de custo. PgBouncer em modo transaction pooling — o mais eficiente em termos de número de conexões físicas ao Postgres — não garante que a mesma conexão física sirva sempre o mesmo cliente lógico entre transações.
Isso quebra a premissa do schema-per-tenant baseado em SET search_path: se o search_path foi definido para o schema do tenant A numa transação, e o PgBouncer devolve essa mesma conexão física para servir o tenant B na transação seguinte, o search_path antigo ainda está lá a menos que a aplicação o resete explicitamente a cada transação. Isso é factível, mas adiciona uma camada de disciplina que o schema-per-tenant, por natureza, deveria evitar ter que se preocupar.
RLS lida melhor com esse cenário porque a variável de sessão app.current_tenant pode ser definida com SET LOCAL, escopado à transação, e descartada automaticamente ao fim dela — compatível nativamente com transaction pooling, conforme a documentação de comandos SET do Postgres.
Independente do modelo escolhido, se você usa RLS, todo índice em tabelas multi-tenant precisa incluir tenant_id como primeira coluna composta. Sem isso, o Postgres aplica o filtro de RLS depois de varrer a tabela inteira, e a query que deveria tocar só as linhas de um tenant acaba fazendo sequential scan em toda a tabela.
Se seu time está desenhando essa camada de dados do zero, vale ter uma revisão de arquitetura antes de escolher o modelo — é o tipo de decisão cara de reverter depois de meses em produção. A Hack Tech Farm ajuda esse desenho em projetos de SaaS sob medida.
Como decidir para o seu SaaS
Um checklist prático de perguntas antes de escolher:
- Quantos tenants você projeta ter em 12 meses — dezenas, centenas ou milhares?
- Existe exigência contratual ou regulatória de isolamento físico auditável para algum cliente?
- Sua equipe tem maturidade para escrever e testar policies de RLS com rigor?
- Seu orçamento de infraestrutura comporta migrations distribuídas em N schemas, ou precisa de pooling agressivo desde o início?
Um modelo híbrido é comum na prática: RLS como padrão para a base de clientes self-service, e schema dedicado (ou até banco dedicado) reservado para contas enterprise que pagam por esse isolamento adicional como parte do contrato. Isso evita pagar o custo operacional do schema-per-tenant em toda a base quando só uma fração dos clientes realmente precisa dele.
Essa decisão também é uma decisão de custo por tenant. Vale cruzar com o cálculo de MRR, churn e LTV: um modelo de isolamento mais caro por tenant só faz sentido se o LTV daquele segmento de cliente sustenta a margem.
Perguntas Frequentes
Row Level Security no Postgres tem impacto de performance?
Sim, mas geralmente pequeno se os índices incluem tenant_id. Sem índice adequado, o filtro de RLS é aplicado após varredura completa da tabela, o que pode ser custoso em tabelas grandes.
É possível migrar de schema-per-tenant para RLS depois?
Sim, mas é um projeto de migração de dados não trivial: exige consolidar tabelas de N schemas em uma única tabela com coluna tenant_id, e reescrever toda a camada de acesso a dados da aplicação.
RLS sozinho é suficiente para compliance como LGPD?
RLS ajuda a controlar acesso a dados, mas compliance com a LGPD envolve também criptografia, auditoria de acesso, retenção e minimização de dados — pontos que RLS não resolve sozinho.
Dá para misturar schema-per-tenant e RLS no mesmo banco?
Sim. É comum usar RLS para a maioria dos tenants em tabelas compartilhadas e reservar schemas dedicados apenas para contas enterprise com exigência contratual de isolamento físico.