RLS (Row Level Security) — o muro que seu banco precisa e que quase ninguém levanta

Existe um tipo de bug que não trava o build, não aparece no log, não cai no Sentry, mas mata o teu produto do dia pra noite: o usuário A abrindo a aplicação e vendo — do nada — dados do usuário B. Transação, saldo, CPF, endereço, o que for. E o pior: em quase todos os projetos onde vi isso acontecer, o problema não era "um bug" no código. Era ausência total de RLS no banco.
Hoje quero falar de Row Level Security, que é literalmente o muro que separa o dado de um usuário do dado do outro — dentro da própria tabela.
Com o vibe coding pegando fogo (que é basicamente pedir pra IA programar tudo e ir empurrando feature sem olhar muito pra baixo do capô), isso ficou ainda mais comum. O pessoal sobe um Supabase, cria umas tabelas, conecta no front e acha que terminou. Só que esqueceu de trancar a porta. Aí qualquer um com a chave pública consegue fazer SELECT * FROM users e levar o banco inteiro embora.
Não é exagero. Eu vi.
Afinal, o que é RLS?
Row Level Security é um recurso nativo do PostgreSQL (e herdado por plataformas como Supabase, Neon, etc.) que permite aplicar regras de acesso a nível de linha. Em vez de controlar acesso só no código da aplicação, você desce o controle pro próprio banco.
Funciona mais ou menos assim: você ativa RLS na tabela, cria policies (políticas) descrevendo quem pode ler, escrever, atualizar e deletar o quê, e o Postgres passa a filtrar cada query automaticamente — sem o dev precisar lembrar disso em todo WHERE.
Em um mundo sem RLS, toda tabela é tipo uma sala com a porta aberta. Quem chega no banco com uma chave válida (a sua anon key, por exemplo) pode, em tese, ler tudo. Quem protege é a tua aplicação — e se ela falhar em um endpoint, já era.
Com RLS, a porta fica trancada por padrão. Mesmo que o front peça tudo, o banco só entrega o que o usuário daquela sessão tem direito de ver.
Pensa no RLS como cinto de segurança do banco. Não é ele que dirige bem — mas quando tem batida, é ele que salva.
Por que isso virou urgência (e não curiosidade)
Se você olhar pra qualquer projeto multi-tenant — SaaS, plataforma financeira, ferramenta colaborativa — o dado de cada cliente mora junto com o dado de todos os outros. Normalmente numa coluna user_id, tenant_id, organization_id, essas coisas.
Se a proteção fica só no front-end ou só na camada de API, basta um endpoint esquecer de filtrar e todos os dados de todos os clientes vazam. E se você expõe o banco direto pro client (padrão Supabase, Firebase, etc.), a tua única linha de defesa passa a ser a regra do banco. Ou seja: sem RLS, não tem defesa.
Tá vendo esse pessoal subindo SaaS em 3 dias usando IA e empurrando pro ar? Muitos desses projetos estão com as tabelas públicas atrás da anon key. Já abri console de alguns "produtos" e consegui listar usuários que não eram meus. Isso não é paranoia, é o cenário real.
O vibe coding acelera entrega, mas também acelera a quantidade de furos de segurança em produção. E LGPD/GDPR não tá nem aí se você "não sabia".
O problema clássico: "mas eu filtro no front"
Esse argumento aparece toda santa vez. "Ah, mas a minha query já tem where user_id = currentUser, então tá seguro".
Não tá. E eu vou mostrar o porquê.
// código do cliente — roda no browser, qualquer um pode inspecionar
const { data } = await supabase
.from('transactions')
.select('*')
.eq('user_id', currentUser.id) // "seguro", né? 🙃
Qualquer pessoa com o DevTools aberto consegue:
- Trocar o
currentUser.idpor outro ID qualquer. - Remover o
.eq()inteiro e fazer umSELECT *. - Usar a própria
anon keydireto em umcurlpra bater no endpoint REST do Supabase/PostgREST.
Se não tem RLS, o banco atende tudo. Porque do ponto de vista dele, a chave é válida e ninguém disse que aquela linha é proibida.
A única coisa que quebra isso de vez é uma policy no banco dizendo: "dessa tabela, só pode ver linha onde user_id = auth.uid()".
Na prática: ligando RLS no Postgres/Supabase
Vamos pro exemplo que mais vejo: uma tabela de transações onde cada usuário deveria ver somente as próprias linhas.
-- 1. Ativa o RLS na tabela (por padrão vem desligado)
alter table transactions enable row level security;
-- 2. Policy de leitura: usuário autenticado só lê as próprias linhas
create policy "select_own_transactions"
on transactions
for select
to authenticated
using (auth.uid() = user_id);
-- 3. Policy de insert: usuário só insere vinculado ao próprio id
create policy "insert_own_transactions"
on transactions
for insert
to authenticated
with check (auth.uid() = user_id);
-- 4. Policy de update/delete: mesma lógica
create policy "update_own_transactions"
on transactions
for update
to authenticated
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
create policy "delete_own_transactions"
on transactions
for delete
to authenticated
using (auth.uid() = user_id);
Duas coisas importantes aqui:
usingé a regra de leitura (quais linhas enxergo);with checké a regra de escrita (quais linhas posso criar/editar).
Se você esquecer o with check no insert/update, o usuário pode criar uma linha em nome de outro e driblar a policy. Detalhe pequeno, consequência gigante.
Multi-tenant: quando o recorte não é por usuário
Em SaaS B2B, o recorte costuma ser por organização. Uma forma comum:
alter table invoices enable row level security;
create policy "select_invoices_same_org"
on invoices
for select
to authenticated
using (
organization_id in (
select organization_id
from memberships
where user_id = auth.uid()
)
);
Aqui a policy diz: você só vê invoice de uma org em que você está listado como membro. Regra no banco, não no front. Subqueries mais pesadas podem custar performance — então vale colocar índice em memberships(user_id, organization_id) e medir.
Role-based: admin vê tudo, usuário comum vê o que é dele
Dá pra combinar RLS com roles custom (via JWT claim no Supabase, por exemplo):
create policy "admins_see_all"
on transactions
for select
to authenticated
using (
auth.jwt() ->> 'role' = 'admin'
or auth.uid() = user_id
);
O or é o pulo do gato — admin vê tudo, o resto vê o próprio. Cuidado só com quem emite o JWT e com o que vai na claim, porque se o front consegue manipular isso, a segurança foi pelo ralo.
Coisas que eu aprendi apanhando
Algumas lições de projeto real (próprio e de cliente):
alter table ... enable row level securitysem policy quebra tudo. Ativar RLS sem declarar nenhuma policy faz o banco bloquear 100% dos acessos pelo client. Então ou você cria as policies antes, ou prepara o "oops" no deploy.service_roleignora RLS. No Supabase, aservice_role keypassa por cima das policies. Ela nunca deve ir pro client. Só no backend, e mesmo assim com cuidado.- Teste como se você fosse o atacante. Logue com o usuário A, pegue o token, tente acessar recurso do usuário B. Se funcionar, você tem bug. Se bloquear com erro de policy, você tá no caminho certo.
- RLS não substitui validação de input. Continua precisando sanitizar, continua precisando de validação no backend. RLS é a última barreira, não a única.
- Views e functions precisam de atenção.
SECURITY DEFINERem função pode burlar RLS sem querer. Leia a doc antes de sair usando.
Se você usa Supabase, dá pra rodar select auth.uid(); dentro do SQL editor logado como um usuário teste pra validar a policy. Teste com usuário real salva tempo.
Vibe coding e o problema de escalar sem olhar pra baixo
Voltando no ponto que me motivou a escrever isso: o vibe coding é ótimo até o momento em que alguém abre o console.
Peguei projetos recentes — alguns até com investidor, clientes pagando — onde o time construiu tudo no grito, feature em cima de feature, IA escrevendo 80% do código. E ninguém parou pra perguntar: "e a policy dessa tabela?"
O resultado costuma ser o mesmo: tabelas sem RLS, anon key exposta, dados de um cliente acessíveis pelo outro. Descobriram na auditoria, no pentest, ou pior — no cliente reclamando que "apareceu dado estranho na minha tela".
Não é culpa da IA. É falta de revisão, falta de critério, e uma certa euforia de que "se funciona, tá bom". Funcionar e estar seguro são duas coisas muito diferentes.
Conclusão
RLS não é feature opcional de banco, é contrato de confiança com o teu usuário. Se você guarda dado de mais de uma pessoa na mesma tabela — e quase todo produto SaaS faz isso — ligar RLS deveria ser o passo zero, não o último.
E vale repetir: validação no front não é segurança. Filtro no backend sem RLS não é defesa em profundidade. A camada do banco é a última linha, e ela precisa estar em pé quando todo o resto falhar.
Se você tá subindo um projeto novo, liga RLS antes de popular a primeira tabela. Se você já tem projeto em produção sem RLS, esse é o teu próximo sprint — não importa o que o board disse. Porque o dia que vazar, ninguém vai lembrar que a feature X era prioridade. Só vão lembrar que você não protegeu o dado.
E isso é o tipo de coisa que a IA não vai resolver por você, por mais vibe coding que seja. Parar, olhar pra base, desenhar as policies e testar continua sendo trabalho de engenharia.
