Pular para o conteúdo
📖 COOKBOOK

RLS — padrões reais que funcionam.

Row Level Security é a feature mais poderosa do Postgres pra multi-tenant. Mostra 5 padrões: own-only, team-shared, public-read, role-based admin, time-window. Todas testadas.

O que vamos fazer

5 padrões de policy RLS que cobrem 90% dos casos de aplicação. Cada padrão vem com SQL pronto pra copiar, explicação de quando usar e onde costuma quebrar.

  1. Own-only — usuário só vê o que ele criou.
  2. Team-shared — membros do mesmo team veem tudo do team.
  3. Public read, owner write — qualquer um lê, só dono altera.
  4. Role-based — admin bypassa, user respeita.
  5. Time-window — só mostra dados dos últimos N dias.

Pré-requisitos

  • Tabela criada no schema do projeto.
  • RLS habilitada na tabela: alter table proj_X.tasks enable row level security;
  • Auth funcionando — as policies usam auth.uid().
⚠️

Atenção: sem enable row level security, as policies existem mas não bloqueiam nada. A tabela continua aberta. Sempre ligue.

Passo a passo

Padrão 1: Own-only (usuário só vê o próprio)

Caso clássico: tabela tasks com coluna user_id. Cada user vê e edita só as próprias tarefas.

schema.sql
alter table proj_X.tasks enable row level security;

-- 1 policy só, cobre SELECT/INSERT/UPDATE/DELETE
create policy tasks_own
  on proj_X.tasks
  for all
  to authenticated
  using (user_id = auth.uid())          -- SELECT/UPDATE/DELETE
  with check (user_id = auth.uid());    -- INSERT/UPDATE

-- Coluna user_id deve ter default
alter table proj_X.tasks
  alter column user_id set default auth.uid();

Com o default auth.uid(), o client pode fazer insert({ title: 'x' }) sem passar user_id — Postgres preenche sozinho com o user logado.

Padrão 2: Team-shared (membros do team veem)

Tabela tasks tem team_id. Cada usuário pode pertencer a vários teams (tabela team_members). Todo mundo do team vê tudo do team.

schema.sql
create table proj_X.team_members (
  team_id uuid not null,
  user_id uuid not null default auth.uid(),
  role text not null default 'member',  -- member | admin
  primary key (team_id, user_id)
);

alter table proj_X.tasks enable row level security;

create policy tasks_team_select
  on proj_X.tasks for select to authenticated
  using (
    team_id in (
      select team_id from proj_X.team_members
      where user_id = auth.uid()
    )
  );

create policy tasks_team_insert
  on proj_X.tasks for insert to authenticated
  with check (
    team_id in (
      select team_id from proj_X.team_members
      where user_id = auth.uid()
    )
  );
💡

Dica de performance: tabelas com muitas linhas e IN (select ...) ficam lentas. Crie índice em team_members(user_id, team_id). Se ainda for lento, extraia pra função STABLE que cacheia por transação (ver Helpers).

Padrão 3: Public read, owner write

Posts de blog: qualquer um (até deslogado) lê, só o autor edita.

schema.sql
alter table proj_X.posts enable row level security;

-- Anyone reads
create policy posts_public_read
  on proj_X.posts for select
  to anon, authenticated
  using (published = true);

-- Owner writes
create policy posts_owner_write
  on proj_X.posts for all
  to authenticated
  using (author_id = auth.uid())
  with check (author_id = auth.uid());

Repare no using (published = true) da policy de leitura: rascunhos (published=false) não aparecem pra ninguém via essa policy. Pro autor ver o próprio rascunho, a policy de owner write também concede SELECT (clauses são OR).

Padrão 4: Admin bypassa, user respeita

Tem coluna role no JWT (você setou via custom claim). Admin vê tudo, user comum só o próprio.

schema.sql
alter table proj_X.invoices enable row level security;

create policy invoices_user_or_admin
  on proj_X.invoices for select to authenticated
  using (
    user_id = auth.uid()
    or (auth.jwt() ->> 'role') = 'admin'
  );

-- Pra escrita, só o dono ou admin
create policy invoices_write
  on proj_X.invoices for all to authenticated
  using (
    user_id = auth.uid()
    or (auth.jwt() ->> 'role') = 'admin'
  )
  with check (
    user_id = auth.uid()
    or (auth.jwt() ->> 'role') = 'admin'
  );

Padrão 5: Janela temporal

Cada user só vê dados dos últimos 30 dias. Útil pra logs, eventos, dados que ficam "frios" rápido.

schema.sql
alter table proj_X.events enable row level security;

create policy events_recent_own
  on proj_X.events for select to authenticated
  using (
    user_id = auth.uid()
    and created_at > now() - interval '30 days'
  );

Helpers úteis

Funções built-in que você usa nas policies:

helpers
auth.uid()    -- uuid do user logado, ou null se anon
auth.role()   -- 'authenticated' | 'anon' | 'service_role'
auth.email()  -- email do user (atalho pra auth.jwt() ->> 'email')
auth.jwt()    -- jsonb com claims inteiras do JWT

Pra reaproveitar lógica complexa (ex: "é admin do team X?"), extraia pra função STABLE:

schema.sql
create or replace function proj_X.is_team_admin(p_team_id uuid)
returns boolean
language sql
stable          -- cacheia por transação — RLS amigável
security invoker
as $$
  select exists (
    select 1 from proj_X.team_members
    where team_id = p_team_id
      and user_id = auth.uid()
      and role = 'admin'
  )
$$;

-- Usar na policy:
create policy tasks_admin_delete on proj_X.tasks
  for delete to authenticated
  using (proj_X.is_team_admin(team_id));

Resultado

Com esses 5 padrões + helpers, você cobre:

  • Apps individuais (todo list, notas, finance pessoal) — own-only.
  • Apps colaborativos (Linear, Notion, Slack) — team-shared.
  • Content sites (blog, marketplace) — public-read + owner-write.
  • Admin dashboards (back-office) — role-based.
  • Logs, eventos, analytics — time-window.

Variações

Combinar com funções Postgres

Lógica de policy ficando grande? Coloque numa função STABLE security invoker e chame na policy. O planejador cacheia o resultado por transação — performance OK.

FORCE ROW LEVEL SECURITY

Por padrão, o owner da tabela (geralmente postgres) bypassa RLS. Pra forçar até pro owner:

schema.sql
alter table proj_X.audit_log force row level security;

Útil pra audit_log ou outras tabelas onde nem o admin do banco deve dar bypass por engano.

Template multi-tabela

Tem 10 tabelas com o mesmo padrão own-only? Em vez de copiar 10 vezes, gere via DO $$ ... $$ que itera por information_schema.tables.

Erros comuns

⚠️

Esquecer WITH CHECK:USING permite SELECT/UPDATE/DELETE mas não restringe o INSERT. Resultado: user A consegue inserir linha com user_id = B. Sempre WITH CHECK em INSERT e UPDATE.

⚠️

USING (true): deixa a tabela aberta. Vejo isso em produção mais do que devia. Se quis dizer "todos podem ler", use FOR SELECT USING (true), não FOR ALL USING (true).

⚠️

RLS não dispara pra service_role: intencional — service_role bypassa tudo. Em scripts admin/cron, isso é desejável. Em código de aplicação, nunca use service_role no client.

⚠️

Tabela criada sem ENABLE: a policy existe mas é ignorada. Resultado: tabela aberta. Cheque com select relname, relrowsecurity from pg_class where relname = 'tasks'.

⚠️

Performance de policy com subquery: using (team_id in (select ...)) em tabela com milhões de linhas vira sequential scan. Solução: índice em team_members(user_id) e extrair pra função STABLE.

Essa página ajudou?