Padrões de Row-Level Security do PostgreSQL para apps multi-tenant
Aprenda Row-Level Security (RLS) no PostgreSQL com padrões práticos para isolamento de tenant e regras por papel, garantindo que o acesso seja aplicado no banco, não apenas na app.

Por que impor acesso no banco de dados importa em apps de negócios
Apps de negócios geralmente têm regras como “usuários só podem ver os registros da sua própria empresa” e “somente gerentes podem aprovar reembolsos”. Muitas equipes aplicam essas regras na interface ou na API e assumem que isso basta. O problema é que cada caminho extra para o banco de dados vira uma nova chance de vazar dados: uma ferramenta de administração interna, um job em background, uma query de analytics, um endpoint esquecido ou um bug que pula uma verificação.
Isolamento de tenant significa que um cliente (tenant) nunca pode ler ou alterar os dados de outro cliente, mesmo por acidente. Controle baseado em papéis significa que pessoas dentro do mesmo tenant ainda têm poderes diferentes, como agentes vs gerentes vs financeiro. Essas regras são fáceis de descrever, mas difíceis de manter perfeitamente consistentes quando vivem em muitos lugares.
A segurança por linha do PostgreSQL (Row-Level Security, RLS) é um recurso do banco de dados que deixa o banco decidir quais linhas uma requisição pode ver ou alterar. Em vez de esperar que toda query na sua app lembre a cláusula WHERE correta, o banco aplica políticas automaticamente.
RLS não é uma armadura mágica para tudo. Não vai projetar seu esquema, substituir a autenticação ou proteger você de alguém que já tem uma role poderosa no banco (como superuser). Também não evita erros de lógica como “alguém pode atualizar uma linha que não consegue selecionar” a menos que você escreva políticas para leitura e escrita.
O que você ganha é uma rede de segurança forte:
- Um conjunto de regras para todo caminho que acessa o banco
- Menos momentos de “ops” quando uma nova feature é lançada
- Auditorias mais claras, porque as regras de acesso estão visíveis em SQL
- Melhor defesa se um bug da API escapar
Há um pequeno custo de configuração. Você precisa de uma forma consistente de passar “quem é esse usuário” e “qual é o tenant” para o banco, e precisa manter as políticas conforme a app cresce. O retorno é grande, especialmente para SaaS e ferramentas internas onde dados sensíveis de clientes estão em jogo.
Noções básicas de Row-Level Security sem jargões
Row-Level Security (RLS) filtra automaticamente quais linhas uma query pode ver ou alterar. Em vez de confiar que cada tela, endpoint da API ou relatório “lembre” as regras, o banco aplica elas para você.
Com a segurança por linha do PostgreSQL, você escreve políticas que são verificadas em todo SELECT, INSERT, UPDATE e DELETE. Se a política diz “este usuário só pode ver linhas do tenant A”, então uma página de admin esquecida, uma nova query ou um hotfix apressado ainda têm as mesmas salvaguardas.
RLS é diferente de GRANT/REVOKE. GRANT decide se uma role pode acessar uma tabela (ou colunas específicas). RLS decide quais linhas dentro dessa tabela são permitidas. Na prática, você costuma usar ambos: GRANT para limitar quem pode tocar a tabela e RLS para limitar o que podem acessar.
Também aguenta o mundo real bagunçado. Views geralmente respeitam RLS porque o acesso à tabela subjacente ainda aciona a política. Joins e subqueries ainda são filtrados, então um usuário não pode “atravessar um join” para chegar aos dados de outro. E a política se aplica independentemente de qual cliente executa a query: código da app, console SQL, job em background ou ferramenta de reporting.
RLS é adequado quando você tem fortes necessidades de isolamento de tenant, múltiplas formas de consultar os mesmos dados ou muitos papéis que compartilham tabelas (comum em SaaS e ferramentas internas). Pode ser exagero para apps minúsculos com um backend confiável, ou para dados que não são sensíveis e nunca saem de um único serviço controlado. No momento em que você tem mais de um ponto de entrada (ferramentas de admin, exports, BI, scripts), RLS normalmente compensa.
Comece mapeando tenants, papéis e propriedade de dados
Antes de escrever uma única política, esclareça quem possui o quê. RLS do PostgreSQL funciona melhor quando seu modelo de dados já reflete tenants, papéis e propriedade.
Comece pelos tenants. Na maioria das apps SaaS, a regra mais simples é: toda tabela compartilhada que contém dados de clientes tem um tenant_id. Isso inclui tabelas “óbvias” como invoices, mas também coisas que as pessoas esquecem, como anexos, comentários, logs de auditoria e jobs em background.
Depois, nomeie os papéis que as pessoas realmente usam. Mantenha o conjunto pequeno e humano: owner, manager, agent, read-only. Esses são papéis de negócio que você posteriormente mapeará para checagens nas políticas (não são a mesma coisa que roles do banco).
Então decida como os registros são pertencidos. Algumas tabelas pertencem a um único usuário (por exemplo, uma nota privada). Outras pertencem a times (por exemplo, uma caixa de entrada compartilhada). Misturar os dois sem um plano leva a políticas difíceis de ler e fáceis de contornar.
Uma maneira simples de documentar suas regras é responder as mesmas perguntas para cada tabela:
- Qual é a fronteira do tenant (qual coluna a aplica)?
- Quem pode ler linhas (por papel e por propriedade)?
- Quem pode criar e atualizar linhas (e sob quais condições)?
- Quem pode deletar linhas (geralmente a regra mais estrita)?
- Quais exceções são permitidas (suporte, automação, exports)?
Exemplo: “Invoices” pode permitir que gerentes vejam todas as invoices do tenant, agentes vejam invoices de clientes atribuídos e usuários read-only apenas leiam, sem editar. Decida desde o início quais regras devem ser estritas (isolamento de tenant, deletes) e quais podem ser flexíveis (visibilidade extra para gerentes). Se você constrói numa ferramenta no-code como AppMaster, esse mapeamento também ajuda a alinhar expectativas da UI com as regras do banco.
Padrões de design para tabelas multi-tenant
RLS multi-tenant funciona melhor quando suas tabelas têm um formato previsível. Se cada tabela armazena o tenant de uma forma diferente, suas políticas viram um quebra-cabeça. Uma forma consistente facilita ler, testar e manter as políticas corretas ao longo do tempo.
Comece escolhendo um identificador de tenant e usando-o em todos os lugares. UUIDs são comuns porque são difíceis de adivinhar e fáceis de gerar em muitos sistemas. Inteiros também funcionam, especialmente para apps internos. Slugs (como "acme") são amigáveis para humanos, mas podem mudar, então trate-os como campo de exibição, não como chave principal.
Para dados escopados por tenant, adicione uma coluna tenant_id em toda tabela que pertença a um tenant, e torne-a NOT NULL sempre que possível. Se uma linha pode existir sem tenant, isso geralmente é um sinal de alerta. Muitas vezes significa que você está misturando dados globais e de tenant na mesma tabela, o que torna as políticas RLS mais difíceis e frágeis.
Indexação é simples mas importante. A maioria das queries em uma app SaaS filtra por tenant primeiro e depois por um campo de negócio como status ou data. Um padrão razoável é um índice em tenant_id, e para tabelas de alto tráfego um índice composto como (tenant_id, created_at) ou (tenant_id, status) baseado nos filtros comuns.
Decida cedo quais tabelas são globais e quais são escopadas por tenant. Tabelas globais comuns incluem countries, currency codes ou definições de planos. Tabelas escopadas por tenant incluem customers, invoices, tickets e qualquer coisa que o tenant possua.
Se você quer um conjunto de regras que permaneça gerenciável, mantenha-o estreito:
- Tabelas escopadas por tenant:
tenant_id NOT NULL, RLS habilitado, políticas sempre checamtenant_id. - Tabelas de referência globais: sem
tenant_id, sem políticas de tenant, somente leitura para a maioria dos papéis. - Tabelas compartilhadas mas controladas: separe tabelas por conceito (evite misturar linhas globais e de tenant).
Se você está construindo com uma ferramenta como AppMaster, essa consistência compensa no modelo de dados também. Uma vez que tenant_id seja um campo padrão, você pode reutilizar os mesmos padrões em módulos sem surpresas.
Passo a passo: crie sua primeira política de tenant
Uma boa primeira vitória com RLS é uma única tabela que só pode ser lida dentro do tenant corrente. O objetivo é simples: mesmo que alguém esqueça um WHERE na API, o banco se recusa a retornar linhas de outros tenants.
Comece com uma tabela que inclua uma coluna tenant_id:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
Quando o RLS é habilitado, o comportamento padrão muitas vezes surpreende: se uma role está sujeita a RLS e não existe política correspondente, SELECT retorna zero linhas (e escritas falham). Isso é o que você quer no começo.
Agora adicione uma política mínima de leitura. Este exemplo assume que sua app define uma variável de sessão como app.tenant_id após o login:
CREATE POLICY invoices_tenant_read
ON invoices
FOR SELECT
USING (tenant_id = current_setting('app.tenant_id')::uuid);
Em seguida, adicione regras de escrita. Em RLS, USING controla quais linhas existentes você pode tocar, e WITH CHECK controla quais novos valores você tem permissão para gravar.
CREATE POLICY invoices_tenant_insert
ON invoices
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY invoices_tenant_update
ON invoices
FOR UPDATE
USING (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
CREATE POLICY invoices_tenant_delete
ON invoices
FOR DELETE
USING (tenant_id = current_setting('app.tenant_id')::uuid);
Políticas são PERMISSIVE por padrão, significando que qualquer política pode permitir o acesso. Escolha RESTRICTIVE quando quiser regras que todas devam passar (útil para adicionar uma segunda barreira como “apenas contas ativas”).
Mantenha políticas pequenas e focadas em papéis. Em vez de uma regra gigante com muitos ORs, crie políticas separadas por audiência (por exemplo, invoices_tenant_read_app_user e invoices_tenant_read_support_agent). É mais fácil testar, revisar e seguro para mudar depois.
Passando contexto de tenant e usuário com segurança
Para RLS funcionar, o banco precisa saber “quem está chamando” e “a qual tenant essa pessoa pertence”. Políticas RLS só podem comparar linhas com valores que o banco consegue ler em tempo de query, então você deve passar esse contexto para a sessão.
Um padrão comum é definir variáveis de sessão após a autenticação e deixar as políticas lê-las com current_setting(). A app valida a identidade (por exemplo, checando um JWT) e então copia apenas os campos necessários (tenant e user IDs) para a conexão com o banco.
-- Run once per request (or per transaction)
SELECT set_config('app.tenant_id', '3f2a0c3e-9c7b-4d3f-9c5c-3c5e9c5d1a11', true);
SELECT set_config('app.user_id', '8d9c6b1a-6b6d-4e32-9c0d-2bfe6f6c1111', true);
SELECT set_config('app.role', 'support_agent', true);
-- In a policy
-- tenant_id column is a UUID
USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
Usar o terceiro argumento true torna a configuração “local” à transação atual. Isso importa se você usa pool de conexões: uma conexão em pool pode ser reutilizada por outra requisição, então você não quer o contexto do tenant de ontem permanecendo.
Populando contexto a partir de claims de JWT
Se sua API usa JWTs, trate os claims como entrada, não como verdade absoluta. Verifique a assinatura do token e expiry primeiro, depois copie apenas os campos necessários (tenant_id, user_id, role) para as configurações de sessão. Evite deixar clientes enviarem esses valores diretamente como headers ou query params.
Contexto ausente ou inválido: negar por padrão
Desenhe políticas de modo que configurações ausentes resultem em zero linhas.
Use current_setting('app.tenant_id', true) para que valores ausentes retornem NULL. Faça cast para o tipo certo (como ::uuid) para que formatos inválidos falhem rápido. E faça a requisição falhar se o contexto de tenant/usuário não puder ser definido, em vez de adotar um padrão por suposição.
Isso mantém o controle de acesso consistente mesmo quando uma query contorna a UI ou um novo endpoint é adicionado depois.
Padrões práticos de papéis que permanecem gerenciáveis
A forma mais fácil de manter políticas RLS legíveis é separar identidade de permissões. Um bom baseline é uma tabela users mais uma tabela memberships que conecta um usuário a um tenant e a um papel (ou vários papéis). Assim suas políticas podem responder a uma pergunta: “O usuário atual tem a membership certa para esta linha?”
Mantenha nomes de papéis ligados a ações reais, não títulos de cargo. “invoice_viewer” e “invoice_approver” tendem a envelhecer melhor que “manager”, porque a política pode ser escrita em termos claros.
Aqui vão alguns padrões de papel que ficam simples conforme a app cresce:
- Apenas proprietário: a linha tem
created_by_user_id(ouowner_user_id) e o acesso checa essa correspondência exata. - Só time: a linha tem
team_ide a política checa que o usuário é membro desse time dentro do mesmo tenant. - Apenas aprovadas: leituras são permitidas somente quando
status = 'approved'e escritas são restritas a aprovadores. - Regras mistas: comece estrito e depois adicione exceções pequenas (por exemplo, “suporte pode ler, mas só dentro do tenant”).
Admins cross-tenant são onde muitas equipes se complicam. Trate-os explicitamente, não como um atalho “superuser” escondido. Crie um conceito separado como platform_admin (global) e exija uma checagem deliberada na política. Melhor ainda, mantenha o acesso entre tenants como somente leitura por padrão e faça escritas exigirem um nível superior.
Documentação importa mais do que parece. Coloque um comentário curto acima de cada política que explique a intenção, não o SQL. “Approvers podem mudar status. Viewers só podem ler invoices aprovadas.” Seis meses depois, essa nota é o que mantém as edições de política seguras.
Se você constrói com uma ferramenta no-code como AppMaster, esses padrões continuam válidos. Sua UI e API podem evoluir rápido, mas as regras do banco permanecem estáveis porque dependem de memberships e significado claro dos papéis.
Cenário de exemplo: um SaaS simples com invoices e suporte
Imagine um SaaS pequeno que atende várias empresas. Cada empresa é um tenant. A app tem invoices (dinheiro) e tickets de suporte (ajuda do dia a dia). Usuários podem ser agentes, gerentes ou suporte.
Modelo de dados (simplificado): cada invoice e ticket tem um tenant_id. Tickets também têm assignee_user_id. A app define o tenant e o usuário atuais na sessão do banco logo após o login.
Veja como RLS muda o risco do dia a dia.
Um usuário do Tenant A abre a tela de invoices e tenta adivinhar um invoice ID do Tenant B (ou a UI envia isso por engano). A query ainda roda, mas o banco retorna zero linhas porque a política exige invoice.tenant_id = current_tenant_id. Não há vazamento com “acesso negado”, apenas resultado vazio.
Dentro de um tenant, papéis restringem mais o acesso. Um gerente pode ver todas as invoices e todos os tickets do tenant. Um agente só vê tickets atribuídos a ele, e talvez seus rascunhos. É aí que equipes frequentemente erram na API, especialmente quando filtros são opcionais.
Suporte é um caso especial. Pode precisar ver invoices para ajudar clientes, mas não deve conseguir alterar campos sensíveis como amount, bank_account ou tax_id. Um padrão prático é:
- Permitir
SELECTem invoices para o papel de suporte (ainda escopado por tenant). - Permitir
UPDATEapenas por um caminho “seguro” (por exemplo, uma view que expõe colunas editáveis, ou uma política de update rigorosa que rejeita mudanças em campos protegidos).
Agora o cenário do “bug acidental da API”: um endpoint esquece de aplicar o filtro de tenant durante um refactor. Sem RLS, pode vazar invoices entre tenants. Com RLS, o banco se recusa a retornar linhas fora do tenant da sessão, então o bug vira uma tela quebrada, não uma violação de dados.
Se você constrói esse tipo de SaaS no AppMaster, você ainda quer essas regras no banco. Checagens na UI ajudam, mas regras no banco são o que seguram quando algo escapa.
Erros comuns e como evitá-los
RLS é poderoso, mas pequenos deslizes podem transformar “seguro” em “surpreendente”. A maioria dos problemas aparece quando uma nova tabela é adicionada, um papel muda ou alguém testa com o usuário de administração errado.
Uma falha comum é esquecer de habilitar RLS em uma tabela nova. Você pode escrever políticas cuidadosas para tabelas principais, depois adicionar uma tabela “notes” ou “attachments” e enviá-la com acesso total. Torne um hábito: tabela nova significa RLS habilitado, mais pelo menos uma política.
Outra armadilha frequente é políticas inconsistentes entre ações. Uma política que permite INSERT mas bloqueia SELECT pode fazer os dados “desaparecerem” logo após serem criados. O oposto também é problemático: usuários podem ler linhas que não conseguem criar, então driblam isso na UI. Pense em fluxos: “criar então ver”, “atualizar então reabrir”, “deletar então listar”.
Cuidado com funções SECURITY DEFINER. Elas rodam com privilégios do dono da função, o que pode contornar RLS se você não for rigoroso. Se usar, mantenha-as pequenas, valide inputs e evite SQL dinâmico a menos que seja realmente necessário.
Também evite confiar apenas em filtragem na aplicação deixando o acesso do banco aberto. Mesmo APIs bem feitas crescem com novos endpoints, jobs em background e scripts de admin. Se a role do banco pode ler tudo, cedo ou tarde algo vai ler tudo.
Para pegar problemas cedo, mantenha as checagens práticas:
- Teste usando a mesma role do DB que sua app usa em produção, não seu usuário admin pessoal.
- Adicione um teste negativo por tabela: um usuário de outro tenant deve ver zero linhas.
- Confirme que cada tabela suporta as ações esperadas:
SELECT,INSERT,UPDATE,DELETE. - Revise o uso de
SECURITY DEFINERe documente por que é necessário. - Inclua “RLS habilitado?” em checklists de code review e migrations.
Exemplo: se um agente de suporte cria uma nota de invoice mas não consegue lê-la de volta, frequentemente é por uma política de INSERT sem a correspondente de SELECT (ou o contexto de tenant não está sendo definido naquela sessão).
Checklist rápido para validar seu setup de RLS
RLS pode parecer correto em revisão e ainda falhar em uso real. A validação é menos sobre ler políticas e mais sobre tentar quebrá-las com contas e queries realistas. Teste do jeito que sua app vai usar, não do jeito que você espera que funcione.
Crie um conjunto pequeno de identidades de teste primeiro. Use pelo menos dois tenants (Tenant A e Tenant B). Para cada tenant, adicione um usuário normal e um admin ou gerente. Se você suporta “support agent” ou “read-only”, adicione também.
Então pressione o RLS com um conjunto pequeno e repetível de checagens:
- Rode as operações principais para cada papel: listar linhas, buscar uma linha por id, inserir, atualizar e deletar. Para cada operação, tente casos “permitidos” e “que devem ser bloqueados”.
- Prove fronteiras de tenant: como Tenant A, tente ler ou modificar dados do Tenant B usando ids que você sabe que existem. Deve retornar zero linhas ou erro de permissão, nunca “algumas linhas”.
- Teste joins procurando vazamentos: faça join de tabelas protegidas com outras (incluindo lookup tables). Confirme que um join não puxe linhas relacionadas de outro tenant via foreign key ou view.
- Cheque que contexto ausente ou errado nega acesso: limpe o contexto de tenant/usuário (o que sua app define por requisição) e tente novamente. “Sem contexto” deve falhar fechado. Tente também um tenant id inválido.
- Confirme desempenho básico: veja planos de query e garanta que índices suportam seu padrão de filtro por tenant (comumente
tenant_idmais o que você ordena ou pesquisa).
Se algum teste te surpreender, corrija a política ou a forma de setar contexto primeiro. Não corrija na UI ou API esperando que as regras do banco “segurem mais ou menos”.
Próximos passos: implantar com segurança e manter consistente
Trate RLS como um sistema de segurança: introduza com cuidado, verifique com frequência e mantenha regras simples o bastante para que sua equipe as siga.
Comece pequeno. Escolha as tabelas cujo vazamento faria mais mal (pagamentos, invoices, dados de RH, mensagens de clientes) e habilite RLS ali primeiro. Vitórias cedo superam um grande rollout que ninguém entende completamente.
Uma ordem prática de rollout costuma ser:
- Primeiro tabelas “owned” (linhas que pertencem claramente a um tenant)
- Tabelas com dados pessoais (PII)
- Tabelas compartilhadas mas filtradas por tenant (reports, analytics)
- Tabelas de join e casos de borda (relationships many-to-many)
- O resto quando o básico estiver estável
Torne os testes obrigatórios. Testes automatizados devem rodar as mesmas queries como tenants e papéis diferentes e confirmar alterações. Inclua checagens de “deve permitir” e “deve negar”, porque bugs mais caros são permissões silenciosas demais.
Mantenha um ponto claro no fluxo de requisição onde o contexto de sessão é definido antes de qualquer query. Tenant id, user id e role devem ser aplicados uma vez, cedo, e nunca supostos depois. Se você setar contexto no meio de uma transação, eventualmente executará uma query com valores ausentes ou obsoletos.
Quando construir com AppMaster, planeje a consistência entre suas APIs geradas e suas políticas PostgreSQL. Padronize como tenant e role são passados ao banco (por exemplo, as mesmas variáveis de sessão para cada endpoint) para que as políticas se comportem igual em todo lugar. Se você está usando AppMaster em appmaster.io, RLS ainda vale como autoridade final para isolamento de tenant, mesmo que também controle acesso na UI.
Finalmente, observe o que falha. Falhas de autorização são sinais úteis, especialmente logo após o rollout. Monitore negações repetidas e investigue se são ataques reais, flows de cliente quebrados ou políticas excessivamente restritivas.
Uma pequena lista de hábitos que ajuda RLS a se manter saudável:
- Mentalidade de negação por padrão, com exceções adicionadas intencionalmente
- Nomes de política claros (tabela + ação + audiência)
- Mudanças nas políticas revisadas como mudanças de código
- Negações logadas e revisadas durante rollout inicial
- Um pequeno conjunto de testes adicionado para cada nova tabela com RLS


