O Row-Level Security no Node.js adiciona uma camada de isolamento diretamente no PostgreSQL. Em vez de depender apenas de filtros escritos pela aplicação, políticas do banco determinam quais linhas cada sessão pode consultar, inserir, atualizar ou excluir.
RLS é especialmente útil em aplicações multi-tenant, sistemas com equipes, contas corporativas e dados regulados. Se uma query esquecer o tenant_id, o banco ainda pode bloquear o acesso. Porém, a proteção só funciona quando o contexto da sessão é configurado corretamente e quando o usuário da aplicação não ignora as políticas.
Neste guia, você aprenderá a habilitar RLS, criar políticas, passar o tenant pelo Node.js, usar transações e pools, testar isolamento, evitar bypass e combinar a proteção com autorização na aplicação.
O que é Row-Level Security?
Row-Level Security, ou RLS, permite aplicar regras por linha em tabelas PostgreSQL. A documentação oficial de Row Security do PostgreSQL explica políticas, permissões e comportamento. A documentação de CREATE POLICY descreve a sintaxe completa.
Para a arquitetura de isolamento, consulte Multi-Tenancy no Node.js. Para conexões e transações, veja Pool PostgreSQL no Node.js e Transações PostgreSQL no Node.js.
Tabela multi-tenant
CREATE TABLE projects (
tenant_id UUID NOT NULL,
id UUID NOT NULL,
name TEXT NOT NULL,
created_by UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (tenant_id, id)
);O tenant_id faz parte da chave e de todos os filtros relevantes.
Habilitando RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;Depois disso, usuários comuns precisam satisfazer uma política para acessar linhas.
Forçando RLS
ALTER TABLE projects FORCE ROW LEVEL SECURITY;Essa opção faz com que o owner da tabela também respeite políticas em situações aplicáveis. Superusers e roles com BYPASSRLS ainda podem ignorá-las.
Contexto por sessão
Uma estratégia comum usa uma configuração customizada:
SELECT set_config(
'app.current_tenant_id',
'11111111-1111-1111-1111-111111111111',
true
);O terceiro argumento true limita o valor à transação atual.
Política de leitura
CREATE POLICY projects_select_policy
ON projects
FOR SELECT
USING (
tenant_id = current_setting(
'app.current_tenant_id',
true
)::uuid
);A expressão USING define quais linhas são visíveis.
Política de inserção
CREATE POLICY projects_insert_policy
ON projects
FOR INSERT
WITH CHECK (
tenant_id = current_setting(
'app.current_tenant_id',
true
)::uuid
);WITH CHECK impede inserir uma linha de outro tenant.
Política de atualização
CREATE POLICY projects_update_policy
ON projects
FOR UPDATE
USING (
tenant_id = current_setting(
'app.current_tenant_id',
true
)::uuid
)
WITH CHECK (
tenant_id = current_setting(
'app.current_tenant_id',
true
)::uuid
);USING controla quais linhas podem ser selecionadas para atualização; WITH CHECK valida o estado final.
Política de exclusão
CREATE POLICY projects_delete_policy
ON projects
FOR DELETE
USING (
tenant_id = current_setting(
'app.current_tenant_id',
true
)::uuid
);Executando com Node.js
async function withTenantTransaction(
pool,
tenantId,
callback
) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query(`
SELECT set_config(
'app.current_tenant_id',
$1,
true
)
`, [tenantId]);
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}O contexto e todas as queries precisam usar a mesma conexão.
Por que usar transação?
Com set_config(..., true), o valor desaparece no commit ou rollback. Isso evita que a próxima requisição que receber a conexão do pool herde o tenant anterior.
Não use SET global no pool
SET app.current_tenant_id = '...';Uma configuração de sessão pode permanecer quando a conexão volta ao pool, criando risco de vazamento. Prefira SET LOCAL ou set_config local dentro da transação.
Validando tenant antes do banco
O tenant precisa vir da identidade autenticada, não do corpo da requisição:
const tenantId = identity.activeTenantId;Confira que o usuário pertence ao tenant e possui uma associação ativa.
RLS não substitui autorização
Uma política por tenant impede acesso cruzado, mas não decide se o usuário pode editar um projeto específico. A aplicação ainda precisa validar funções, escopos e regras do recurso.
Políticas por usuário
CREATE POLICY documents_owner_policy
ON documents
FOR ALL
USING (
tenant_id = current_setting(
'app.current_tenant_id', true
)::uuid
AND owner_id = current_setting(
'app.current_user_id', true
)::uuid
);Políticas muito complexas podem prejudicar desempenho e manutenção.
Políticas por role
É possível criar políticas para roles PostgreSQL específicas:
CREATE POLICY support_read_policy
ON projects
FOR SELECT
TO support_reader
USING (true);Não mapeie cada usuário final para uma role do banco em aplicações grandes sem necessidade.
Usuário da aplicação
A role usada pelo Node.js não deve ser superuser, owner desnecessário nem possuir BYPASSRLS.
Separando role de migration
Use uma credencial de migration com privilégios de DDL e outra credencial restrita para a aplicação. Consulte Migrações de Banco no Node.js.
Default deny
Quando RLS está habilitado e nenhuma política permite a operação, o acesso é negado. Esse comportamento é uma vantagem de segurança.
Políticas permissivas
Por padrão, políticas permissivas são combinadas com OR. Se duas políticas permitem conjuntos diferentes, a linha pode ser liberada por qualquer uma.
Políticas restritivas
Políticas restritivas podem ser combinadas com AND em versões compatíveis:
CREATE POLICY active_tenant_policy
ON projects
AS RESTRICTIVE
FOR ALL
USING (
current_setting('app.tenant_active', true) = 'true'
);Confirme a semântica na versão do PostgreSQL.
current_setting ausente
O segundo argumento true retorna NULL quando a configuração não existe:
current_setting('app.current_tenant_id', true)A comparação com NULL não libera linhas, mantendo o comportamento fechado.
Insert com tenant automático
A aplicação pode preencher explicitamente:
INSERT INTO projects (
tenant_id,
id,
name,
created_by
) VALUES (
current_setting('app.current_tenant_id')::uuid,
$1,
$2,
current_setting('app.current_user_id')::uuid
);Isso reduz a chance de usar um tenant vindo do payload.
Views e funções
Views, funções SECURITY DEFINER e procedures podem alterar a forma como RLS é aplicada. Revise owner, search_path e privilégios.
SECURITY DEFINER
Uma função com SECURITY DEFINER executa com privilégios do owner. Ela pode contornar políticas se mal configurada. Use owner sem privilégios excessivos e defina search_path seguro.
Prepared statements
RLS é avaliada durante a execução de acordo com contexto e plano. Teste prepared statements em conexões reutilizadas e não dependa de valores fixados na preparação.
Índices
Políticas adicionam condições às queries. Crie índices iniciando pelo tenant:
CREATE INDEX idx_projects_tenant_created
ON projects (tenant_id, created_at DESC);EXPLAIN
Analise planos sob a role da aplicação e com contexto real:
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, name
FROM projects
ORDER BY created_at DESC
LIMIT 20;Particionamento
RLS e particionamento podem coexistir, mas políticas, pruning e índices precisam ser testados.
Cache
RLS protege o banco, não o Redis ou cache em memória. Toda chave de cache ainda deve incluir tenant.
Filas
Workers precisam criar a mesma transação com contexto por mensagem. Não confie em um tenant global mutável.
Jobs administrativos
Um job que percorre vários tenants deve abrir um escopo por tenant. Evite usar uma role com bypass apenas por conveniência.
Suporte e impersonação
Se o suporte precisa acessar dados, use fluxo auditado, escopo temporário e políticas específicas. Não desative RLS globalmente.
Logs
Registre tenant ID, usuário, operação e resultado, sem incluir dados sensíveis. Consulte Logs com Pino no Node.js.
Métricas
Monitore erros de política, transações sem contexto, queries lentas e tentativas de acesso cruzado. Evite tenant ID como label de alta cardinalidade.
Auditoria
RLS impede acesso, mas não registra automaticamente todas as tentativas. Combine logs da aplicação e auditoria do banco conforme requisitos.
Testes de integração
Use a mesma role da produção. Testes com superuser podem passar mesmo quando a política está errada.
Teste cruzado
test('tenant A não lê dados do tenant B', async () => {
const projectB = await createProjectForTenant(tenantB);
const result = await withTenantTransaction(
pool,
tenantA,
client => client.query(
'SELECT * FROM projects WHERE id = $1',
[projectB.id]
)
);
assert.equal(result.rowCount, 0);
});Teste de inserção cruzada
Tente inserir tenant_id diferente do contexto e confirme erro de política.
Teste de conexão reutilizada
Use a mesma conexão em transações sequenciais para tenants diferentes e confirme que o contexto anterior não permanece.
Teste sem contexto
Execute uma query sem definir tenant e confirme que nenhuma linha é retornada ou que a operação é rejeitada.
Teste de owner
Confirme que a role real não é owner ou que FORCE ROW LEVEL SECURITY está aplicado conforme o desenho.
Erros comuns
- Role com BYPASSRLS: políticas não protegem a aplicação.
- SET de sessão no pool: tenant anterior pode vazar.
- Somente USING: inserts ou updates podem gravar tenant errado.
- Teste com superuser: falhas não são detectadas.
- Tenant vindo do corpo: cliente escolhe o contexto.
- Cache sem tenant: RLS não protege a resposta cacheada.
- Função SECURITY DEFINER insegura: políticas são contornadas.
Boas práticas
- Use uma role restrita.
- Habilite e force RLS quando adequado.
- Configure contexto dentro de transação.
- Use SET LOCAL ou set_config local.
- Crie USING e WITH CHECK.
- Mantenha índices com tenant.
- Separe credencial de migration.
- Proteja cache e filas.
- Teste com a role real.
- Mantenha autorização na aplicação.
Conclusão
O Row-Level Security no Node.js adiciona uma barreira de isolamento diretamente no PostgreSQL. Mesmo que uma query esqueça o filtro, a política pode impedir que linhas de outro tenant sejam lidas ou modificadas.
A proteção depende de configuração cuidadosa: role sem bypass, contexto transacional, políticas de leitura e escrita, índices e testes com conexões reutilizadas. Com RLS, filtros explícitos, cache segmentado e autorização de domínio, aplicações multi-tenant ganham defesa em profundidade contra vazamentos de dados.



