Skip to content

ADR: Uso de service_role vs RLS — Decisão Arquitetural

Status: Aprovado
Data: 2026-04-06 (atualizado 2026-04-07 — Fases 0–4 concluídas) Autores: Equipe OLP
Revisão: Obrigatória a cada nova Edge Function que use createSupabaseSystem()


Contexto

O Supabase oferece dois modos de acesso ao banco:

ModoChaveRLSRisco
Autenticado (createSupabaseClient)ANON_KEY + JWT do usuário✅ AtivoBaixo — usuário só vê o que a policy permite
Sistema (createSupabaseSystem)SERVICE_ROLE_KEY❌ BypassaAlto — acesso irrestrito a todas as tabelas

Regra fundamental: service_role é exceção controlada, nunca padrão. Toda nova Edge Function DEVE usar createSupabaseClient(req) a menos que se enquadre em uma categoria legítima documentada abaixo.


Categorias de Uso

Categoria 1: PRE-AUTH — Legítimo ✅

Justificativa: O usuário ainda não está autenticado. Não existe JWT, não existe cookie. Usar createSupabaseClient(req) resultaria em um cliente anônimo sem permissões.

Por que não criar policies públicas? A tabela usuarios contém CPF, telefone, email. Uma policy USING (true) exporia dados pessoais de todos os usuários via API REST direta.

Arquivos:

  • send-otp/index.ts — Busca usuário por CPF/INEP/CNPJ
  • verify-otp/index.ts — Valida OTP e gera JWT
  • verify-password/index.ts — Login por senha (fluxo legado)
  • reset-password/index.ts — Reset de senha sem sessão
  • portal-login/index.ts — Login do portal (matrícula + DN)
  • portal-send-otp/index.ts — OTP para responsáveis
  • portal-verify-otp/index.ts — Validação OTP portal

Veredicto: Manter. Sem alternativa segura.


Categoria 2: LOGGING — Legítimo ✅

Justificativa: A tabela logs_transacoes tem zero INSERT policies por design. Nenhum papel deve poder inserir logs arbitrários via API — isso evita log injection (atacante inserindo entradas falsas no audit trail).

O logging é fire-and-forget: a Edge Function registra o log ao final da operação, usando dados já validados. O service_role é necessário porque o INSERT é bloqueado para todos os papéis via RLS.

Arquivos:

  • _shared/logging-helper.tsregistrarLog() (chamado por ~40 Edge Functions)
  • _shared/logging-helper.ts — Cache de geolocalização (ip_geo_cache)

Veredicto: Manter. Restrição de design intencional.


Categoria 3: FEATURE FLAGS + CANARY — Migrado ✅

Justificativa original: As tabelas não tinham SELECT policies para authenticated.

Correção aplicada:

  1. Migration 20260406_feature_flags_select: Criadas SELECT policies para todas as tabelas de flags/canary
  2. Migration de código: Todas as queries em permissions.ts e feature-gate.ts migradas de supabaseSystem para supabaseUser
  3. Assinatura de requireFeatureFlag() alterada: aceita cliente autenticado em vez de system
  4. ~21 Edge Functions atualizadas para passar cliente autenticado ao requireFeatureFlag()

Tabelas: feature_flags, feature_flag_canary, canary_groups, canary_group_escolas, canary_group_usuarios

Status: ✅ Completamente migrado. Zero uso de service_role para feature flags.


Categoria 4: PERMISSÕES — Migrado ✅

Correções aplicadas:

  1. usuarios_escola_sub_permissoes — Migrado para supabaseUser (sprint anterior)
  2. usuario_papeis — Policy morta corrigida (auth.uid()auth.jwt()->>'sub'), queries migradas para supabaseUser
  3. resolveMenuForUser() — Assinatura simplificada: removido parâmetro supabaseSystem (não mais necessário)

Arquivos:

  • _shared/permissions.ts — Todas as queries usam supabaseUser
  • gestao-usuarios-escola/ — Usa supabaseSystem para operações admin (legítimo — admin CRUD)
  • user-permissions/ — Migrado para supabaseUser (removido createSupabaseSystem)

Status: ✅ Completamente migrado.


Categoria 5: TABELAS SEM POLICIES — Parcialmente Resolvido 🟡

Padrão "Defesa em Profundidade": Tabelas com RLS ativo mas zero policies. Qualquer acesso via PostgREST retorna vazio. O acesso real é exclusivamente via Edge Functions com service_role.

Tabelas migradas (resolvidas):

  • coordenador_cores — ✅ 3 policies criadas (SELECT, INSERT, UPDATE). Migrado user-profile, tarefas-escola, tarefas-helpers.

Tabelas ainda pendentes:

  • escola_anotacoes — Notas internas da escola (usado em tarefas-escola e tarefas-helpers)
  • portal_login_tentativas — Tentativas de login do portal (contexto pre-auth)
  • portal_rate_metrics — Métricas de rate limiting
  • portal_otps — OTPs do portal
  • portal_alert_cooldown — Cooldown de alertas

Arquivos pendentes:

  • tarefas-escola/index.ts — Ainda usa supabaseSystem para escola_anotacoes
  • tarefas-helpers.ts — Ainda usa supabaseSystem para escola_anotacoes
  • portal-login/index.ts — Insere em portal_login_tentativas
  • portal-security.ts — Rate limiting com métricas

Roadmap: Criar SELECT/INSERT policies scoped para escola_anotacoes e migrar.


Categoria 6: ORQUESTRADORES — Parcialmente Migrável 🔄

Arquivos: me/index.ts

O endpoint /me é o mais quente do sistema (chamado em toda navegação). Usa createSupabaseSystem() porque precisa cruzar dados de múltiplas tabelas das categorias 3-5 que não tinham policies.

Após as correções das categorias 3-4: Parte das queries do /me pode migrar para supabaseUser. As queries de feature flags e sub-permissões agora têm policies.

Status: Migração parcial planejada após validação em produção das novas policies.


Critérios de Decisão

Quando service_role é LEGÍTIMO

CritérioExemplo
Usuário não autenticado (pre-auth)send-otp, portal-login
INSERT bloqueado por design (anti-injection)logs_transacoes, login_otps
Operação de sistema sem contexto de usuárioCrons, webhooks externos
Tabela de configuração interna do sistemacron_status

Quando service_role é DÍVIDA TÉCNICA

CritérioExemplo
Tabela com RLS ativo + zero policiescoordenador_cores
SELECT de dados não-sensíveis por autenticadosfeature_flags (corrigido)
Query que já tem equivalente com supabaseUser no mesmo arquivosub_permissoes (corrigido)

Quando service_role é PROIBIDO

CenárioMotivo
Frontend (client-side)Expõe chave no browser
Edge Function onde o usuário JÁ está autenticado E a tabela TEM policiesBypassa proteção sem necessidade
Operações de CRUD iniciadas pelo usuárioDeve respeitar RLS do papel

Inventário Completo

ArquivoCategoriaStatus
send-otp/1. Pre-Auth✅ Legítimo
verify-otp/1. Pre-Auth✅ Legítimo
verify-password/1. Pre-Auth✅ Legítimo
reset-password/1. Pre-Auth✅ Legítimo
portal-login/1. Pre-Auth✅ Legítimo
portal-send-otp/1. Pre-Auth✅ Legítimo
portal-verify-otp/1. Pre-Auth✅ Legítimo
logging-helper.ts2. Logging✅ Legítimo
permissions.ts (flags)3. Feature Flags✅ Migrado para supabaseUser
feature-gate.ts3. Feature Flags✅ Migrado para supabaseUser
permissions.ts (sub_perms)4. Permissões✅ Migrado para supabaseUser
permissions.ts (usuario_papeis)4. Permissões✅ Migrado para supabaseUser
user-permissions/4. Permissões✅ Migrado para supabaseUser
gestao-usuarios-escola/4. Permissões (admin)✅ Legítimo (admin CRUD)
user-profile/ (coordenador_cores)5→✅✅ Migrado para supabaseUser
tarefas-escola/ (coordenador_cores)5→✅✅ Migrado para supabaseUser
tarefas-escola/ (escola_anotacoes)5. Sem Policies🟡 Dívida técnica
tarefas-helpers.ts (escola_anotacoes)5. Sem Policies🟡 Dívida técnica
portal-login/ (tentativas)5. Sem Policies🟡 Dívida técnica
me/index.ts6. Orquestrador✅ Parcialmente migrado (flags + permissões via supabaseUser)

Roadmap de Migração

Concluído

  • [x] RLS policies para usuarios_escola_sub_permissoes
  • [x] Migrar permissions.ts sub_perms para supabaseUser
  • [x] SELECT policies para feature flags + canary
  • [x] Migrar queries de feature flags em permissions.ts para supabaseUser
  • [x] Migrar requireFeatureFlag() em feature-gate.ts para supabaseUser
  • [x] Migrar ~21 Edge Functions para passar cliente autenticado ao requireFeatureFlag()
  • [x] Corrigir SELECT policy de usuario_papeis (auth.uid()auth.jwt()->>'sub')
  • [x] Migrar queries de usuario_papeis em permissions.ts para supabaseUser
  • [x] Criar policies para coordenador_cores (SELECT, INSERT, UPDATE)
  • [x] Migrar user-profile, tarefas-escola, tarefas-helpers para supabaseUser
  • [x] Remover parâmetro supabaseSystem de resolveMenuForUser()
  • [x] Migrar admin-feature-flags de supabaseSystem para createSupabaseClient(req) + 12 policies admin-only
  • [x] Migrar notificacoes (create_internal, create_batch) de supabaseSystem para createSupabaseClient(req) + 1 policy admin INSERT
  • [x] Migrar coordenador-videos: removido createSupabaseSystem, RPC increment_video_view chamado via RLS client (SECURITY DEFINER)
  • [x] Migrar escola-pagamentos (parcial): recalculate_due_dates migrado para RLS (2 policies UPDATE: escola_assinaturas_escola_update_own, escola_faturas_escola_update_own). request_plan_change mantém supabaseSystem — reclassificado como Categoria 2 (notificação cross-tenant)
  • [x] Migrar set-password: 100% RPCs SECURITY DEFINER (get_own_password_hash, update_own_password_hash, get_own_password_history, save_to_password_history). Zero createSupabaseSystem.
  • [x] Migrar change-password (parcial): operações de senha via RPCs. supabaseSystem mantido apenas para HIBP fire-and-forget (ADR Cat. 2 — INSERT notificacoes)
  • [x] Migrar reset-password (parcial): idem change-password
  • [x] Migrar gestao-turmas: 100% RLS. createSupabaseSystem removido. Admin removido do requireRole (menor privilégio — turmas são conteúdo pedagógico). Policy legada turmas_admin_readonly removida. listSeries migrado para createSupabasePublic(). escola_id override eliminado (zero-trust).

Backlog

  • [ ] Criar policies para escola_anotacoes e migrar tarefas-escola
  • [ ] Avaliar migração residual do /me (logging permanece com service_role por design)
  • [ ] Migrar ~18 Edge Functions autenticadas de supabaseSystem para createSupabaseClient(req) (ver §Dívida Técnica abaixo)

Padrão de Nomenclatura de Variáveis

Status: Aprovado (2026-04-07) Regra: Máximo 5 nomes padronizados. Qualquer outro alias é PROIBIDO.

Nome da variávelConstrutorQuando usarRLS
supabasecreateSupabaseClient(req)Padrão — 99% das operações autenticadas✅ Ativo
supabaseSystemcreateSupabaseSystem()Exceção — Pre-Auth, Logging, Crons❌ Bypassa
supabaseStoragecreateSupabaseSystem()Storage only — uploads/deletes em buckets❌ Bypassa
supabasePubliccreateSupabasePublic()Lookups públicos sem token✅ Ativo
supabasePortalcreateSupabasePortalAuth(req)Portal autenticado (cookie olp_mural)✅ Ativo

Regras

  1. supabase é o nome padrão. Não precisa de sufixo porque RLS é o comportamento default.
  2. supabaseSystem causa desconforto intencional ao ler — alerta que bypassa RLS.
  3. supabaseStorage é alias documentado de createSupabaseSystem(), exclusivo para .storage.from(). PROIBIDO usar para .from("tabela").
  4. PROIBIDO: supabaseRLS, supabaseUser, supabaseRLSForFlags, ou qualquer outro alias não listado acima.

Justificativa

  • supabaseRLS foi eliminado porque todo createSupabaseClient já respeita RLS por definição. O sufixo não adicionava informação e criava falsa impressão de um tipo especial de client.
  • supabaseStorage foi mantido porque é auto-explicativo e restrito a operações de Storage (que requerem service_role por design do Supabase).

Dívida Técnica: CRUD Autenticado com supabaseSystem

Audit de 2026-04-07: ~20 Edge Functions autenticadas usam supabaseSystem para CRUD padrão, bypassando RLS sem justificativa documentada.

Estas functions autenticam o usuário (requireRole / extractAuthenticatedUser) mas usam supabaseSystem para todas as queries:

ArquivoGravidadeNota
gestao-alunosALTAsupabaseSystem para TODOS os helpers CRUD
gestao-turmasALTA✅ Migrado para RLS (Fase 5 — 2026-04-07). Admin removido (menor privilégio). Zero createSupabaseSystem.
gestao-resultadosMÉDIAImport/export de resultados
escola-pagamentosMÉDIA✅ Parcialmente migrado (Fase 3 — recalculate_due_dates via RLS; request_plan_change mantém supabaseSystem — Categoria 2 cross-tenant)
coordenador-videosMÉDIA✅ Migrado para RLS (Fase 3 — 2026-04-07)
notificacoesMÉDIA✅ Migrado para RLS (Fase 2 — 2026-04-07)
change-passwordMÉDIA✅ Migrado para RPCs (Fase 4 — 2026-04-07). supabaseSystem mantido apenas para HIBP fire-and-forget (Cat. 2)
set-passwordMÉDIA✅ 100% migrado para RPCs (Fase 4 — 2026-04-07). Zero supabaseSystem.
admin-feature-flagsMÉDIA✅ Migrado para RLS (Fase 1 — 2026-04-07)
admin-dashboardMÉDIAQueries em portal_rate_metrics, escolas
admin-usuariosMÉDIAQueries em usuario_papeis, usuarios
admin-usuarios-escolaMÉDIACRUD em papeis, usuario_papeis, usuarios

Funções limpas na Fase 0 (dead code removido — 2026-04-07)

ArquivoO que foi feito
especialista-olimpiadasRemovido import/instanciação morta de createSupabaseSystem
especialista-headersIdem
especialista-cursosIdem
escola-dashboardIdem
eventos-calendarioIdem
eventos-calendario-trialIdem

Impacto: As policies RLS dessas tabelas nunca são testadas em produção — existem mas são bypassadas.

Migração requer (projeto separado):

  1. Criar/validar policies RLS para cada tabela envolvida
  2. Testar cada function contra as policies
  3. Deploy gradual por módulo

Referências

  • supabase/functions/_shared/supabase-client.ts — Definição dos 5 construtores
  • docs/security/RLS_POLICIES.md — Policies por tabela e papel
  • docs/security/RLS_DESIGN_GUIDE.md — Guia de design de RLS
  • Migration 20260406221825 — Fix de privilege escalation em sub_permissoes
  • Migration 20260406_feature_flags_select — SELECT policies para feature flags
  • Migration 20260406 — Fix usuario_papeis + policies coordenador_cores