RLS no Supabase: como configurar e testar se o seu app vaza dados
O que é RLS, em uma frase
RLS (Row Level Security, segurança por linha) é a regra do banco que decide quais linhas cada pessoa pode ler, criar, alterar ou apagar. Sem ela, quem pode acessar a tabela acessa a tabela inteira.
Por que isso importa no Lovable, no Bolt e no Base44
Apps feitos com essas ferramentas costumam falar com o banco direto do navegador. Para isso, a chave publicável do Supabase vai junto com o site. Ela foi feita para ficar exposta, mas com uma condição: o Supabase pede que você ligue o RLS em todas as tabelas antes de publicar (Supabase, API keys).
A documentação do Supabase resume o risco: uma tabela num schema exposto, sem RLS, pode ser lida e alterada por qualquer papel com permissão sobre ela (Supabase, Row Level Security). E em projetos antigos, tabelas novas no schema public podem nascer com todas as permissões dadas aos papéis do cliente.
Os exemplos abaixo usam uma tabela pedidos com uma coluna user_id, que guarda o dono de cada linha. Troque pelos nomes do seu app.
Passo 1: ache as tabelas expostas
No painel do Supabase, abra o Security Advisor. Ele também roda pela CLI com supabase db advisors (Supabase, Database Advisors). Os alertas que importam aqui:
| Alerta | O que quer dizer |
|---|---|
0013_rls_disabled_in_public |
Tabela pública com RLS desligado. Quem tem a URL do projeto lê, edita e apaga as linhas |
0007_policy_exists_rls_disabled |
Há política, mas o RLS está desligado. A política não vale |
0008_rls_enabled_no_policy |
RLS ligado e nenhuma política. A API não devolve nada |
0010_security_definer_view |
View que roda com o poder de quem a criou e passa por cima do RLS |
0002_auth_users_exposed |
View que expõe a tabela de usuários do login |
Comece pelos de nível ERROR: o 0013, o 0010 e o 0002.
No Lovable Cloud, a lista de políticas fica em More → Database → Manage → Policies. Ela é só de leitura: as mudanças são pedidas ao Lovable no chat (Lovable, Database access). Os passos abaixo servem para você saber o que pedir e o que conferir.
Passo 2: ligue o RLS
No SQL Editor:
alter table public.pedidos enable row level security;
Atenção: a partir daqui, nada passa pela chave publicável até existir uma política (Supabase, Row Level Security). Se o app já está no ar, ele vai parar de mostrar os pedidos. Faça os passos 2 a 4 juntos, de preferência num projeto de teste.
Passo 3: revise as permissões dos papéis
O Supabase usa dois papéis para quem vem do navegador:
anon: quem não fez login;authenticated: quem fez login.
Tire tudo e devolva só o necessário. Se só usuário logado mexe em pedidos:
revoke all on table public.pedidos from anon, authenticated;
grant select, insert, update, delete on table public.pedidos to authenticated;
Passo 4: escreva uma política por operação
A doc do Supabase recomenda uma política para cada operação e sempre com to, para dizer a que papel ela vale. No caso “cada usuário só mexe nos próprios pedidos”:
create policy "ler os próprios pedidos"
on public.pedidos for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "criar pedido em nome próprio"
on public.pedidos for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "alterar os próprios pedidos"
on public.pedidos for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
create policy "apagar os próprios pedidos"
on public.pedidos for delete
to authenticated
using ( (select auth.uid()) = user_id );
using e with check não são a mesma coisa:
usingdecide quais linhas que já existem a pessoa pode ver, alterar ou apagar.with checkdecide como a linha pode ficar depois de criada ou alterada.
No update, escreva os dois, como faz a doc do Supabase. Se o with check faltar, o PostgreSQL usa o using também para a linha nova (PostgreSQL, CREATE POLICY). Funciona, mas esconde a intenção: quem ler a política depois não sabe se a omissão foi de propósito.
Um erro comum é criar só a política de select e achar que a tabela está protegida. Cada operação precisa da sua.
Passo 5: corrija as views
View criada no schema public roda, por padrão, com o poder de quem a criou e ignora o RLS de quem consulta. A correção indicada pelo Supabase é criar a view com security_invoker=on (Supabase, Database Advisors):
create view public.resumo_pedidos
with (security_invoker = on) as
select user_id, count(*) as total
from public.pedidos
group by user_id;
Nunca exponha auth.users numa view pública. Se precisa de nome e foto do usuário, crie uma tabela de perfil com RLS própria.
Passo 6: crie índice nas colunas das políticas
Política roda em toda consulta. Para não deixar o app lento, o Supabase pede duas coisas (Supabase, Row Level Security):
- Índice em toda coluna usada nas políticas:
create index pedidos_user_id_idx on public.pedidos using btree (user_id);
- Escrever
(select auth.uid())em vez deauth.uid(). Assim o Postgres calcula o valor uma vez por consulta, e não uma vez por linha.
Passo 7: teste se ainda vaza
Faça os testes no seu próprio projeto.
Teste 1: como estranho. Use a chave publicável, a mesma que está no site, e chame a API REST do Supabase (Supabase, REST API):
curl "https://SEU-PROJETO.supabase.co/rest/v1/pedidos?select=*" \
-H "apikey: SUA_CHAVE_PUBLICAVEL"
O esperado, com o revoke do passo 3, é um erro de permissão ou uma lista vazia. Se vierem pedidos, qualquer visitante do site consegue o mesmo.
Teste 2: como outro cliente. Crie os usuários A e B. Com A, crie um pedido. Entre com B e tente ver, editar e apagar o pedido de A pelo app, trocando o ID no endereço se o app usar IDs na URL. O Lovable pede esse mesmo teste antes de publicar (Lovable, Security best practices).
Rode de novo o Security Advisor no fim. Os alertas do passo 1 devem ter sumido.
O erro “new row violates row-level security policy”
Esse erro aparece depois que o RLS é ligado e é bom sinal: o banco recusou uma linha. Pela documentação do PostgreSQL, quando a expressão do with check não é verdadeira para a linha nova, o banco devolve erro em vez de gravar (PostgreSQL, CREATE POLICY).
Quase sempre a causa é uma destas:
- não há política de
insertpara o papel que está gravando; - o app não manda o
user_id, ou manda um diferente do usuário logado; - a pessoa não está logada, e a requisição chega como
anon.
Não resolva com uma política que aceita tudo (with check (true)). Corrija o app para mandar o user_id certo, ou preencha a coluna com um valor padrão auth.uid().
O mesmo erro em storage quer dizer que falta política no bucket de arquivos. A lógica é a mesma.
Cuidado com a chave secreta e as Edge Functions
A chave secreta (sb_secret_..., ou a antiga service_role) passa por cima de todas as políticas. O Supabase diz para nunca usá-la no navegador, num app distribuído ou no controle de versão (Supabase, API keys).
Ela tem lugar em função de servidor. Mas aí o RLS não protege mais nada: a função tem que checar sozinha quem chamou e o que essa pessoa pode fazer (Lovable, Security best practices). Revise toda Edge Function que usa a chave secreta.
Se achar a chave secreta no código do site, troque-a: crie uma nova, substitua em todo lugar e só depois desative a antiga.
Peça o diagnóstico da Heartscale
No diagnóstico da Heartscale, nossa equipe lê todas as tabelas, políticas, views e funções do seu Supabase ou Lovable Cloud. Também testa o acesso com a chave pública e confere onde a chave secreta é usada. Você recebe a lista do que está aberto, por gravidade, e o preço da correção.
O diagnóstico custa R$ 398 e é abatido se você fechar a correção. Só trabalhamos em app de quem prova ser dono.