Supabase com RLS desligada: qualquer pessoa lê o banco inteiro
Você subiu o app. O login funciona, cada usuário vê os dados dele, a tela está certa. E está mesmo — no navegador.
O problema é que o navegador não é o único jeito de falar com o seu banco.
A chave que aparece no F12 não é um vazamento
Abra o seu app, aperte F12, procure por supabaseKey. Ela está lá, no meio do
JavaScript. Isso assusta na primeira vez, mas é assim que tem que ser: a
chave anônima (anon key) do Supabase é publicável por design, do mesmo jeito
que a chave publicável do Stripe. Ela não é uma senha. Ela só diz de qual
projeto a requisição veio.
Quem decide o que essa chave pode ler não é ela — é a Row Level Security.
E a RLS vem desligada quando você cria uma tabela.
O que "desligada" significa na prática
Com a RLS desligada, a tabela responde a qualquer requisição que traga a chave anônima. Não interessa quem está logado, nem se alguém está logado.
O seu app filtra por usuário porque o seu código pede assim:
const { data } = await supabase
.from('mensagens')
.select('*')
.eq('user_id', user.id) // <- o filtro está aqui, no cliente
Esse .eq() é uma conveniência da sua tela. Não é uma regra do banco. Quem
falar com o Supabase direto simplesmente não escreve essa linha:
curl "https://SEU-PROJETO.supabase.co/rest/v1/mensagens?select=*" \
-H "apikey: SUA_ANON_KEY" \
-H "Authorization: Bearer SUA_ANON_KEY"
Se voltar a lista de mensagens de todo mundo, a RLS está desligada nessa tabela. E as duas coisas que esse comando precisa — a URL do projeto e a chave anônima — estão as duas no JavaScript da sua página.
Por que isso é tão comum em app feito com IA
Não é descuido de quem usa Lovable, Bolt, Replit, Base44, Create.xyz ou qualquer outra plataforma que monta o app inteiro com Supabase de backend — e também não é exclusivo de quem usa essas plataformas prontas: o mesmo acontece com Cursor, Windsurf ou qualquer assistente de código gerando o schema direto. É a ordem em que as coisas acontecem.
O assistente gera o schema, gera as telas, e tudo funciona na primeira execução. RLS ligada quebraria essa primeira execução: com RLS e sem política nenhuma, a tabela responde vazio, o app parece bugado e a pessoa pede para o assistente "consertar". O caminho de menor atrito é a tabela aberta.
Some a isso o fato de que o app parece certo — cada usuário vê só o que é dele, na tela — e você tem um problema que não se manifesta em nenhum teste manual. Só aparece quando alguém pergunta ao banco sem passar pela sua tela.
Como verificar as suas tabelas
No painel: Table Editor → cada tabela mostra um aviso de "RLS disabled" quando está aberta. É a checagem mais rápida, e o lugar onde você vê todas de uma vez.
Por fora: o curl acima, uma tabela por vez. É mais chato, mas é o que
prova o comportamento real — inclusive nos casos em que a RLS está ligada e a
política é permissiva demais, que o aviso do painel não pega.
Ligar a RLS não é o fim
Este é o erro seguinte, e ele é mais silencioso.
Com a RLS ligada e nenhuma política, a tabela nega tudo. O app quebra. A saída que aparece em muito tutorial é esta:
-- não faça isso
create policy "acesso" on mensagens
for all using (true);
Isso liga a RLS e devolve exatamente o comportamento de antes: using (true) é
verdadeiro para qualquer requisição, autenticada ou não. O painel para de
reclamar e o banco continua aberto.
A política precisa amarrar a linha ao dono dela:
alter table mensagens enable row level security;
create policy "dono lê as próprias mensagens" on mensagens
for select using (auth.uid() = user_id);
create policy "dono escreve as próprias mensagens" on mensagens
for insert with check (auth.uid() = user_id);
Duas coisas que se esquece aqui:
for selectnão cobre escrita. Uma política de leitura bem feita convive com uminsertliberado, e aí qualquer um grava na sua tabela. Cada operação precisa da sua.usingfiltra o que já existe;with checkvalida o que está entrando. Sem owith checknoinsert, dá para gravar uma linha com ouser_idde outra pessoa.
A chave que realmente não pode aparecer
Existe uma segunda chave no painel do Supabase: a service_role. Essa
ignora a RLS inteira, por design — ela existe para o seu backend.
Se ela estiver no código do front, nada do que está acima importa: as políticas não se aplicam a quem a usa. Procure por ela no seu bundle. Se encontrar, rotacione a chave antes de qualquer outra coisa — remover do código não invalida a que já foi publicada.
O resumo
- A
anon keyno F12 é normal. Aservice_roleno F12 é incidente. - RLS vem desligada; o app funcionar não é evidência de que ela está ligada.
using (true)cala o aviso sem resolver o problema.- Cada operação (
select,insert,update,delete) precisa da sua política.
