Lá em 2021, um cliente SaaS chegou até mim uns seis meses após o lançamento. O produto deles era uma ferramenta de gerenciamento de projetos. Interface legal, onboarding sólido, retenção decente. Aí um dos usuários beta notou algo: ao mexer no project_id em uma requisição GET, conseguia ler dados de outro usuário. Sem bypass de auth. Sem SQL injection. Só uma política faltando. A tabela tinha RLS ativado, mas a política SELECT estava aberta, USING (true). Alguém tinha copiado de um tutorial e nunca mais olhou.
Aquele incidente ficou comigo. Desde então, trato RLS como arquitetura, não como improviso. E depois de construir mais de 12.000 sites e apps na Seahawk, tenho um punhado de políticas que escrevo quase reflexivamente, antes de escrever uma única linha de código frontend.
Esta é essa lista.
Por que RLS vale a pena a complicação
Supabase roda em PostgreSQL, o que significa que Row Level Security é um recurso de banco de dados de primeira classe, não um agregado. Quando você ativa RLS em uma tabela e um usuário a consulta através do cliente Supabase, cada linha é filtrada através de suas políticas automaticamente. Não importa o que sua camada de API faz ou deixa de fazer.
Este último ponto é o cerne. Trabalhei com equipes usando REST, GraphQL, edge functions e background jobs acessando o mesmo banco de dados. Aplicar controle de acesso na camada de aplicação em todos esses é um pesadelo de coordenação. Aplicar na camada de banco de dados é simplesmente... feito.
Honestamente, a complicação de escrever políticas se paga sozinha na primeira vez que um dev júnior esquece de adicionar uma cláusula WHERE.
Tem uma coisa que as pessoas entendem errado. Ativar RLS sem nenhuma política não significa "acesso aberto". Significa sem acesso nenhum para papéis normais. Cada linha é negada por padrão. Então se você ativa RLS e a app quebra na hora, é por isso.
As quatro tabelas que sempre protejo com RLS primeiro
Nem tudo precisa do mesmo nível de escrutínio. Mas estes quatro tipos de tabela ficam bloqueados antes de eu mexer em qualquer outra coisa.
- Tabelas de perfil de usuário (profiles, users,
accounts): A óbvia. Usuários devem ler sua própria linha. Talvez admins consigam ler todas. Ninguém deve escrever no perfil de outra pessoa. - Tabelas de recurso/conteúdo (projects, documents,
posts): Seja qual for a "coisa" central na sua app. A propriedade geralmente é direta aqui. - Tabelas de billing e assinatura: Se você usa Stripe e armazena dados de plano, status de assinatura ou histórico de faturas em Supabase, precisa estar bem trancado. Vi apps acidentalmente expondo datas de término de trial para outros usuários.
- Logs de auditoria: São somente leitura para usuários (se é que). Só service role deveria escrever neles.
As políticas que realmente escrevo
1. A política somente-dono (Meu padrão mais usado)
Esta é a que escrevo mais do que qualquer outra. Premissa simples: usuários só conseguem ver linhas que possuem.
`` create policy "Users can view own rows" on profiles for select using (auth.uid() = user_id); ``
auth.uid() é um helper Supabase que retorna o UUID do usuário atualmente autenticado. Limpo, rápido, indexado se user_id é indexado. Combino isso com uma política insert que define user_id como auth.uid() por padrão, então usuários não conseguem inserir linhas fingindo ser outra pessoa.
`` create policy "Users can insert own rows" on profiles for insert with check (auth.uid() = user_id); ``
A cláusula with check é para operações de escrita. using é para leituras. Muita gente mistura essas duas e acaba com políticas que parecem corretas mas não impedem inserções ruins de verdade.
2. A Política de Org/Team (Apps Multi-Tenant)
É aqui que as coisas ficam interessantes. Para qualquer SaaS multi-tenant, preciso que os usuários vejam linhas que pertencem à sua organização, não só a eles mesmos.
O padrão que adoto: uma tabela de junção memberships que vincula usuários a organizações.
`` create policy "Org members can view org resources" on projects for select using ( exists ( select 1 from memberships where memberships.org_id = projects.org_id and memberships.user_id = auth.uid() ) ); ``
Seahawk teve um projeto fintech onde a org tinha dezenas de usuários, alguns com funções só de leitura, outros com acesso de escrita. Estendemos esse padrão com uma coluna role em memberships e a usamos direto na política. Assim um role viewer não conseguia rodar UPDATE ou DELETE no nível do banco de dados, fim de papo. Não é imposição da API. É imposição do banco.
3. A Política de Public Read / Owner Write
Para conteúdo que é publicamente visível mas editável só pelo dono. Posts de blog, perfis públicos, listagens de produtos.
``` create policy "Anyone can read published posts" on posts for select using (published = true);
create policy "Authors can update own posts" on posts for update using (auth.uid() = author_id) with check (auth.uid() = author_id); ```
Duas políticas separadas. Vejo pessoas tentando combinar essas em uma e acabam com lógica difícil de raciocinar. Mantenha elas separadas. Postgres faz OR delas automaticamente para a mesma operação quando precisa.
4. A Escape Hatch do Service Role
Algumas operações legitimamente precisam contornar RLS. Background jobs, webhooks, scripts admin. Para essas eu uso a chave service_role, que desativa RLS completamente.
Mas aqui está a coisa: eu nunca exponho a chave service_role em código frontend. Nunca. Ela fica em variáveis de ambiente só no lado servidor. Eu já revisei codebases onde estava hardcoded num diretório pages/ do Next.js. Seu banco inteiro, completamente aberto.
Se você está usando Supabase edge functions, pode usar o service role client dentro delas com segurança, porque edge functions rodam no servidor. Vale a pena ler de cabo a rabo a documentação própria do Supabase sobre auth e service roles se você não leu.
5. A Política de Admin Override
Para apps com painel admin, eu adiciono uma política que dá aos admins acesso total, verificado contra um role armazenado nos metadados JWT do usuário ou numa tabela separada user_roles.
`` create policy "Admins can do everything" on projects for all using ( exists ( select 1 from user_roles where user_roles.user_id = auth.uid() and user_roles.role = 'admin' ) ); ``
Eu costumava armazenar roles nos JWT custom claims, o que é mais rápido (sem subquery), mas significa que você tem que re-emitir o JWT toda vez que um role muda. Para a maioria das apps, a subquery é ok. Se você está vendo problemas de performance em escala, JWT custom claims via Supabase Auth hooks é o caminho.
Erros Comuns que Cometi (E Vi Outros Cometer)
Deixe-me ser direto sobre as coisas que realmente me morderam ou meus clientes.
- Esquecer de políticas UPDATE e DELETE. É fácil escrever uma política SELECT e achar que acabou. Não acabou. Teste as quatro operações: SELECT, INSERT, UPDATE, DELETE. Eu uso o teste de política integrado do dashboard Supabase agora, mas por anos eu estava escrevendo SQL puro no psql e testando manualmente.
- Confusão entre
USINGeWITH CHECK.USINGfiltra quais linhas uma query consegue ver.WITH CHECKvalida se uma operação de escrita é permitida. Para UPDATE, você precisa dos dois: USING para controlar quais linhas podem ser alvo, WITH CHECK para controlar como a linha fica depois da atualização. - Loops de política recursiva. Se sua política na tabela A faz query na tabela B, e a tabela B tem uma política que faz query na tabela A, você vai ter recursão infinita. Eu bati nisso uma vez com tabelas
teamseteam_membersque se referenciavam. O fix: use funções com security definer para quebrar o ciclo. - Não testar como usuário anônimo. Supabase deixa você usar o role anônimo (anon). Sempre teste suas políticas como
authenticatedeanon. Eu uso Postman com tokens de auth diferentes para simular isso, alternando entre sem token, um token de usuário válido, e o token de outro usuário. - Deixar RLS desativado em buckets de armazenamento. RLS se aplica à tabela
storage.objectstambém. Se você criar um bucket Supabase Storage e deixar essa tabela desprotegida, qualquer pessoa pode ler seus arquivos "privados" se adivinhar o caminho. Aprendi isso na prática em um projeto cliente que armazenava documentos enviados por usuários.
Como Testo Minhas Policies Antes de Enviar
Este é meu processo real, não uma checklist teórica.
- Escrevo a policy no editor SQL do Supabase.
- Abro uma segunda aba do navegador e faço login como um usuário de teste diferente.
- Tento acessar dados que deveriam ser bloqueados. Confirmo que está bloqueado.
- Tento acessar dados que deveriam estar visíveis. Confirmo que funciona.
- Executo UPDATE e DELETE em uma linha que não possuo. Deve falhar.
- Verifico os logs do Supabase para erros de política de segurança em nível de linha violada.
Para qualquer coisa complicada, especialmente policies de org multi-tenant, escrevo um pequeno script de teste usando o cliente supabase-js com duas sessões de usuário diferentes e verifico os resultados esperados. Leva uns 20 minutos para escrever, economiza horas de debug em produção.
Quando NÃO Usar RLS
RLS nem sempre é a ferramenta certa.
Se você estiver construindo uma ferramenta admin interna onde todos os usuários são funcionários confiáveis, RLS adiciona complexidade sem muito retorno. Uma simples verificação de auth no servidor funciona bem. Se seu modelo de dados é tão complexo que policies exigem subqueries com 5 níveis de profundidade, você pode estar melhor aplicando controle de acesso em uma camada API com decomposição adequada de serviços.
Além disso: se você está usando Supabase puramente como backend com sua própria API na frente (nunca expondo a URL Supabase ou chave anon aos clientes), RLS é opcional. A API se torna sua camada de segurança. Dito isto, ainda adiciono policies básicas nesses casos porque defesa em profundidade vale a pena.
Olha, RLS é uma ferramenta. Não uma religião. Use-a onde ela torna seu sistema mais simples e seguro. Não faça cópia de tutorial só porque um tutorial mandou.
FAQ
Eu preciso de RLS se estou usando Supabase apenas com uma API no servidor?
Não necessariamente. Se seu frontend nunca toca Supabase diretamente e tudo passa por seu próprio servidor, sua API é a camada de segurança. Mas ainda recomendo adicionar pelo menos policies baseadas em proprietário como uma segunda linha de defesa. Se alguém encontrar um bug na sua API, RLS captura o que passa.
RLS afeta o desempenho?
Pode, se suas policies envolvem subqueries caras em tabelas grandes. O fix é quase sempre indexação. Certifique-se de que as colunas usadas nas condições de sua policy (user_id, org_id, etc.) têm índices. Em um projeto no ano passado, adicionar um índice em org_id reduziu o tempo de avaliação de policy de ~40ms para menos de 2ms em uma tabela com 800k linhas.
Posso usar RLS com Supabase Realtime?
Sim. Subscrições Realtime respeitam policies RLS. Se um usuário se subscreve a mudanças em uma tabela, ele só receberá eventos para linhas que suas policies permitem ver. Esta é uma das decisões de design genuinamente boas na arquitetura do Supabase.
Qual é a diferença entre `for all` e escrever policies separadas?
for all cria uma única policy cobrindo SELECT, INSERT, UPDATE e DELETE. É conveniente para padrões de admin override. Para tudo mais, escrevo policies separadas por operação porque as condições geralmente são diferentes. SELECT pode permitir leituras públicas enquanto INSERT requer proprietário. Policies separadas são mais fáceis de entender quando algo vai errado às 23h.
Como debugo uma policy que está bloqueando requisições que não deveria?
Primeiro: verifique se auth.uid() está realmente retornando um valor. Se o usuário não está autenticado, retorna null e a maioria das policies falhará. Segundo: temporariamente defina a policy para USING (true) para confirmar que a query funciona. Terceiro: adicione uma policy de teste que registra os valores que você está verificando (usando uma função security definer que lança um notice). Os logs do dashboard Supabase também mostram violações RLS, o que torna isso muito menos doloroso do que costumava ser.
---
Segurança em nível de linha não é um trabalho glamoroso. Ninguém escreve posts de blog sobre a violação que não aconteceu. Mas aquele incidente em 2021 com dados de projeto expostos me ensinou que a diferença entre "RLS ativado" e "RLS feito corretamente" é maior do que a maioria das pessoas pensa. Essas políticas fecham essa lacuna. Pelo menos fazem para mim.
