O que é?
Row Level Security (RLS) é uma feature nativa do Postgres desde a versão 9.5 (2016). Ela permite escrever políticas SQL que dizem quais linhas (rows) um usuário pode ver, criar, atualizar ou deletar — e o banco aplica isso automaticamente em toda query.
Sem RLS, é sua aplicação que filtra:
// Você precisa lembrar de filtrar em CADA query
const { data } = await db
.from('tasks')
.select('*')
.eq('user_id', currentUser.id) // ← esqueceu? vaza tudo.
Com RLS, o banco filtra:
// O SELECT é "puro" — RLS adiciona o filtro
const { data } = await db.from('tasks').select('*')
// Postgres traduz pra: SELECT * FROM tasks WHERE user_id = auth.uid()
A política vive no banco. Toda forma de acesso — SDK, API REST, SQL direto, Studio — passa pela mesma regra.
Por que importa?
- Centraliza a regra. Mudança de permissão é uma policy, não vasculhar 87 endpoints.
- Defesa em profundidade. Mesmo se um endpoint tem bug e esqueceu de filtrar, RLS pega.
- Funciona pra REST e SQL. Sua API REST, suas funções RPC, suas migrations — todas respeitam.
- Auditoria mais simples. Mostra as
policys pra auditor, ele entende o modelo de permissão num arquivo.
Princípio: "trust no client". O cliente envia o JWT, mas é o servidor que decide o que cada JWT pode ler. RLS é onde essa decisão acontece.
Como funciona no SuperDB
O fluxo, do request ao banco:
- Cliente envia request com
Authorization: Bearer <jwt> - PostgREST (ou o serviço da API) valida a assinatura do JWT
- Antes de executar a query, PostgREST seta variáveis de sessão:
SET LOCAL request.jwt.claims = '{...}'SET LOCAL role = 'authenticated'(ouanon)
- A função
auth.uid()retornarequest.jwt.claims->>'sub' - Postgres adiciona o filtro das policies à query
Funções helper que o SuperDB instala no schema auth:
auth.uid() -- UUID do usuário (do claim sub)
auth.role() -- 'authenticated' | 'anon' | 'service_role'
auth.email() -- email do usuário (do claim email)
auth.jwt() -- JSON completo dos claims
Ativar RLS numa tabela
RLS não é ativa por padrão. Toda tabela nova tem que ser explicitamente protegida:
CREATE TABLE tasks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid REFERENCES auth.users NOT NULL,
title text NOT NULL,
done boolean DEFAULT false
);
-- LIGAR RLS (sem isso, a policy abaixo é ignorada)
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
-- Criar a policy
CREATE POLICY "own tasks" ON tasks
FOR ALL -- aplica em SELECT/INSERT/UPDATE/DELETE
USING (user_id = auth.uid()) -- linhas que o user PODE LER/UPDATE/DELETE
WITH CHECK (user_id = auth.uid()); -- linhas que ele pode INSERT/UPDATE pra
USING vs WITH CHECK: USING filtra o que ele vê. WITH CHECK filtra o que ele cria/altera. Faltando WITH CHECK, um user pode criar uma row com user_id de outro user (e depois não vê — mas o outro vê).
Quando usar (e quando não)
Use SEMPRE em…
- Tabelas com dados de usuário.
tasks,messages,orders,posts— tudo que tem "dono" - Tabelas de relacionamento.
team_members,shared_with— onde permissão depende de outras tabelas - Dados sensíveis. Mesmo "só admins vêem" — escreva a policy
Pode pular em…
- Tabelas de config global.
app_settings,country_codes,feature_flags— leitura pública, escrita só por admin (useGRANTbásico) - Views read-only públicas. Onde RLS da tabela base já cobre
Mesmo "tabela só leitura pra todo mundo": o SuperDB recomenda ENABLE ROW LEVEL SECURITY + POLICY USING (true). Explícito é melhor que implícito — e o Studio mostra um warning vermelho em tabelas sem RLS.
Exemplos práticos
1. Own — só vê o que é seu
CREATE POLICY "users see own tasks"
ON tasks FOR ALL
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());
2. Team — todos do time vêem
CREATE POLICY "team sees team tasks"
ON tasks FOR SELECT
USING (
team_id IN (
SELECT team_id FROM team_members WHERE user_id = auth.uid()
)
);
CREATE POLICY "team writes own"
ON tasks FOR INSERT
WITH CHECK (
team_id IN (
SELECT team_id FROM team_members WHERE user_id = auth.uid()
)
);
3. Public-read, owner-write — blog clássico
-- Todo mundo (logado ou não) lê posts publicados
CREATE POLICY "public reads published"
ON posts FOR SELECT
USING (published = true);
-- Mas só o autor cria/edita/apaga
CREATE POLICY "author writes own"
ON posts FOR INSERT WITH CHECK (author_id = auth.uid());
CREATE POLICY "author updates own"
ON posts FOR UPDATE USING (author_id = auth.uid());
CREATE POLICY "author deletes own"
ON posts FOR DELETE USING (author_id = auth.uid());
Armadilhas comuns
- Esqueceu
ENABLE ROW LEVEL SECURITY. Tabela sem RLS ativa ignora todas as policies — retorna tudo. Sempre chequeSELECT relrowsecurity FROM pg_class WHERE relname = 'tasks';. - Policy sem
WITH CHECKem INSERT. Permite criar rows comuser_idde outro user. Sempre escreva os dois quando fazFOR ALL. - Service-role bypassa RLS. Por design — admin script pode tudo. Mas se você usa service-role no client achando que "RLS protege", está errado. Veja service vs anon.
- Policies são OR, não AND. Se você tem 2 policies SELECT, a row aparece se qualquer uma liberar. Pra restringir, use
FOR ... USING (a AND b)em uma policy só. - Performance: RLS adiciona um
WHEREao plano. Garante que as colunas usadas emUSINGestão indexadas — sem índice emuser_id, query lenta. - Subqueries em policies podem ser caras. Policy que faz
SELECT FROM teams WHERE...em toda row é lenta com muitas rows. Considere materializar permissão. - RLS não cobre coluna. RLS é por linha. Pra esconder colunas (ex:
credit_card), use views ouREVOKEcoluna.
Anti-padrão: nunca confie em "o frontend nunca pediria essa row". Frontend pode ser modificado, request pode ser feito direto com cURL. RLS é o garantidor — não o frontend.
Termos relacionados
- Multi-tenant — RLS é a camada de segurança dentro do schema do tenant
- JWT — carrega o
auth.uid()que policies leem - Service-role vs anon — service-role bypassa RLS
- Cookbook RLS — receitas prontas pra padrões comuns
- Postgres docs — Row Security