RLS
Isolamento por workspace com Row Level Security
Row Level Security (RLS) garante que cada workspace só acesse seus próprios dados. É a única camada de isolamento multi-tenant — o frontend não filtra por workspace_id por confiança, ele filtra porque o banco também filtra.
Como funciona
Toda tabela com dado de negócio tem:
- Uma coluna
workspace_id - Policies RLS que verificam se o usuário pertence ao workspace
get_user_workspace_ids()
Função central que devolve os workspaces do usuário autenticado:
create function get_user_workspace_ids()
returns setof uuid as $$
select m.workspace_id
from members m
join workspaces w on w.id = m.workspace_id
where m.user_id = auth.uid()
and m.is_active = true
and w.suspended_at is null
$$ language sql security definer stable;Repare no w.suspended_at is null: workspace suspenso perde acesso pela RLS, não por checagem no frontend. É o que faz o bloqueio por inadimplência valer mesmo para quem chama a API direto.
Padrão de policy
O padrão mais comum, usado nas tabelas de dado de negócio:
create policy "workspace_isolation" on deals
for all
to authenticated
using (
workspace_id in (select get_user_workspace_ids())
or is_super_admin(auth.uid())
);O bypass do super admin fica dentro da mesma policy, com or — não existe policy separada para ele. E a função exige o argumento: is_super_admin(auth.uid()), nunca is_super_admin().
☠️ FOR ALL com USING e sem WITH CHECK já foi furo em produção
USING decide quais linhas você enxerga. WITH CHECK decide o que você pode gravar. Quando a policy é for all e só tem using, o Postgres usa a mesma expressão para os dois — ou seja, qualquer membro ativo do workspace pode gravar qualquer coluna e apagar a linha inteira.
Foi exatamente isso que aconteceu na tabela workspaces (CRM-388): qualquer membro ativo gravava plan, suspended_at, name e settings, e dava DELETE na linha. A correção foi trocar a policy única por quatro policies, uma por comando, com o with check restrito a admin, gestor ou super admin:
create policy "workspaces_select" on workspaces
for select to authenticated
using (id in (select get_user_workspace_ids()) or is_super_admin(auth.uid()));
create policy "workspaces_update" on workspaces
for update to authenticated
using (id in (select get_user_admin_workspace_ids()) or is_super_admin(auth.uid()))
with check (id in (select get_user_admin_workspace_ids()) or is_super_admin(auth.uid()));
-- delete e insert idem, restritos a quem pode de verdadeA regra: se numa tabela nem todo membro pode escrever, for all não serve. Separe por comando e escreva o with check. Ver a migration 20260723_000168.
☠️ to public não é o mesmo que to authenticated. Policy criada sem a cláusula to nasce public, o que inclui o papel anônimo. Onde existe caminho anônimo legítimo (o link público de proposta), ele é explícito e restrito a colunas nomeadas — não é um public acidental.
Multi-tenancy
workspaces é a unidade de isolamento. Um usuário pode pertencer a mais de um workspace via members, e cada workspace tem os dados isolados dos demais.
Super admin
O super admin entra em modo de impersonação no client (workspaceStore.enterAsAdmin) e enxerga o workspace alvo como se fosse membro — mas nenhum registro fantasma é criado em members. O acesso é puramente pela policy.
☠️ Prova de RLS feita com super admin não vale nada — ele passa por cima de tudo. Para provar isolamento, use set local role authenticated com as claims de um usuário real, dentro de uma transação com rollback.
Nunca bypasse RLS no código da aplicação. Se um fluxo precisa de acesso elevado, ou o usuário é super admin (e a policy resolve), ou o fluxo roda numa edge function com service_role.
Toda tabela nova nasce com RLS habilitado e ao menos uma policy de isolamento antes de ir para produção. Tabela sem RLS em produção é vazamento garantido.
☠️ Função SECURITY DEFINER ignora RLS. Ela roda com os privilégios de quem a criou, não de quem chama — por isso toda função desse tipo que não é RPC de usuário revoga execute de public, anon e authenticated. Cuidado: revoke from public não remove o grant padrão de anon e authenticated; os três precisam ser revogados explicitamente.