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 usecreateSupabaseSystem()
Contexto
O Supabase oferece dois modos de acesso ao banco:
| Modo | Chave | RLS | Risco |
|---|---|---|---|
Autenticado (createSupabaseClient) | ANON_KEY + JWT do usuário | ✅ Ativo | Baixo — usuário só vê o que a policy permite |
Sistema (createSupabaseSystem) | SERVICE_ROLE_KEY | ❌ Bypassa | Alto — 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/CNPJverify-otp/index.ts— Valida OTP e gera JWTverify-password/index.ts— Login por senha (fluxo legado)reset-password/index.ts— Reset de senha sem sessãoportal-login/index.ts— Login do portal (matrícula + DN)portal-send-otp/index.ts— OTP para responsáveisportal-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.ts—registrarLog()(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:
- Migration
20260406_feature_flags_select: Criadas SELECT policies para todas as tabelas de flags/canary - Migration de código: Todas as queries em
permissions.tsefeature-gate.tsmigradas desupabaseSystemparasupabaseUser - Assinatura de
requireFeatureFlag()alterada: aceita cliente autenticado em vez de system - ~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:
usuarios_escola_sub_permissoes— Migrado parasupabaseUser(sprint anterior)usuario_papeis— Policy morta corrigida (auth.uid()→auth.jwt()->>'sub'), queries migradas parasupabaseUserresolveMenuForUser()— Assinatura simplificada: removido parâmetrosupabaseSystem(não mais necessário)
Arquivos:
_shared/permissions.ts— Todas as queries usamsupabaseUsergestao-usuarios-escola/— UsasupabaseSystempara operações admin (legítimo — admin CRUD)user-permissions/— Migrado parasupabaseUser(removidocreateSupabaseSystem)
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). Migradouser-profile,tarefas-escola,tarefas-helpers.
Tabelas ainda pendentes:
escola_anotacoes— Notas internas da escola (usado emtarefas-escolaetarefas-helpers)portal_login_tentativas— Tentativas de login do portal (contexto pre-auth)portal_rate_metrics— Métricas de rate limitingportal_otps— OTPs do portalportal_alert_cooldown— Cooldown de alertas
Arquivos pendentes:
tarefas-escola/index.ts— Ainda usasupabaseSystemparaescola_anotacoestarefas-helpers.ts— Ainda usasupabaseSystemparaescola_anotacoesportal-login/index.ts— Insere emportal_login_tentativasportal-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ério | Exemplo |
|---|---|
| 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ário | Crons, webhooks externos |
| Tabela de configuração interna do sistema | cron_status |
Quando service_role é DÍVIDA TÉCNICA
| Critério | Exemplo |
|---|---|
| Tabela com RLS ativo + zero policies | coordenador_cores |
| SELECT de dados não-sensíveis por autenticados | feature_flags (corrigido) |
Query que já tem equivalente com supabaseUser no mesmo arquivo | sub_permissoes (corrigido) |
Quando service_role é PROIBIDO
| Cenário | Motivo |
|---|---|
| Frontend (client-side) | Expõe chave no browser |
| Edge Function onde o usuário JÁ está autenticado E a tabela TEM policies | Bypassa proteção sem necessidade |
| Operações de CRUD iniciadas pelo usuário | Deve respeitar RLS do papel |
Inventário Completo
| Arquivo | Categoria | Status |
|---|---|---|
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.ts | 2. Logging | ✅ Legítimo |
permissions.ts (flags) | 3. Feature Flags | ✅ Migrado para supabaseUser |
feature-gate.ts | 3. 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.ts | 6. 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.tssub_perms parasupabaseUser - [x] SELECT policies para feature flags + canary
- [x] Migrar queries de feature flags em
permissions.tsparasupabaseUser - [x] Migrar
requireFeatureFlag()emfeature-gate.tsparasupabaseUser - [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_papeisempermissions.tsparasupabaseUser - [x] Criar policies para
coordenador_cores(SELECT, INSERT, UPDATE) - [x] Migrar
user-profile,tarefas-escola,tarefas-helpersparasupabaseUser - [x] Remover parâmetro
supabaseSystemderesolveMenuForUser() - [x] Migrar
admin-feature-flagsdesupabaseSystemparacreateSupabaseClient(req)+ 12 policies admin-only - [x] Migrar
notificacoes(create_internal, create_batch) desupabaseSystemparacreateSupabaseClient(req)+ 1 policy admin INSERT - [x] Migrar
coordenador-videos: removidocreateSupabaseSystem, RPCincrement_video_viewchamado via RLS client (SECURITY DEFINER) - [x] Migrar
escola-pagamentos(parcial):recalculate_due_datesmigrado para RLS (2 policies UPDATE:escola_assinaturas_escola_update_own,escola_faturas_escola_update_own).request_plan_changemantémsupabaseSystem— 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). ZerocreateSupabaseSystem. - [x] Migrar
change-password(parcial): operações de senha via RPCs.supabaseSystemmantido apenas para HIBP fire-and-forget (ADR Cat. 2 — INSERTnotificacoes) - [x] Migrar
reset-password(parcial): idemchange-password - [x] Migrar
gestao-turmas: 100% RLS.createSupabaseSystemremovido. Admin removido dorequireRole(menor privilégio — turmas são conteúdo pedagógico). Policy legadaturmas_admin_readonlyremovida.listSeriesmigrado paracreateSupabasePublic(). escola_id override eliminado (zero-trust).
Backlog
- [ ] Criar policies para
escola_anotacoese migrartarefas-escola - [ ] Avaliar migração residual do
/me(logging permanece com service_role por design) - [ ] Migrar ~18 Edge Functions autenticadas de
supabaseSystemparacreateSupabaseClient(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ável | Construtor | Quando usar | RLS |
|---|---|---|---|
supabase | createSupabaseClient(req) | Padrão — 99% das operações autenticadas | ✅ Ativo |
supabaseSystem | createSupabaseSystem() | Exceção — Pre-Auth, Logging, Crons | ❌ Bypassa |
supabaseStorage | createSupabaseSystem() | Storage only — uploads/deletes em buckets | ❌ Bypassa |
supabasePublic | createSupabasePublic() | Lookups públicos sem token | ✅ Ativo |
supabasePortal | createSupabasePortalAuth(req) | Portal autenticado (cookie olp_mural) | ✅ Ativo |
Regras
supabaseé o nome padrão. Não precisa de sufixo porque RLS é o comportamento default.supabaseSystemcausa desconforto intencional ao ler — alerta que bypassa RLS.supabaseStorageé alias documentado decreateSupabaseSystem(), exclusivo para.storage.from(). PROIBIDO usar para.from("tabela").- PROIBIDO:
supabaseRLS,supabaseUser,supabaseRLSForFlags, ou qualquer outro alias não listado acima.
Justificativa
supabaseRLSfoi eliminado porque todocreateSupabaseClientjá respeita RLS por definição. O sufixo não adicionava informação e criava falsa impressão de um tipo especial de client.supabaseStoragefoi mantido porque é auto-explicativo e restrito a operações de Storage (que requeremservice_rolepor design do Supabase).
Dívida Técnica: CRUD Autenticado com supabaseSystem
Audit de 2026-04-07: ~20 Edge Functions autenticadas usam
supabaseSystempara CRUD padrão, bypassando RLS sem justificativa documentada.
Estas functions autenticam o usuário (requireRole / extractAuthenticatedUser) mas usam supabaseSystem para todas as queries:
| Arquivo | Gravidade | Nota |
|---|---|---|
gestao-alunos | ALTA | supabaseSystem para TODOS os helpers CRUD |
gestao-turmas | ✅ Migrado para RLS (Fase 5 — 2026-04-07). Admin removido (menor privilégio). Zero createSupabaseSystem. | |
gestao-resultados | MÉDIA | Import/export de resultados |
escola-pagamentos | ✅ Parcialmente migrado (Fase 3 — recalculate_due_dates via RLS; request_plan_change mantém supabaseSystem — Categoria 2 cross-tenant) | |
coordenador-videos | ✅ Migrado para RLS (Fase 3 — 2026-04-07) | |
notificacoes | ✅ Migrado para RLS (Fase 2 — 2026-04-07) | |
change-password | ✅ Migrado para RPCs (Fase 4 — 2026-04-07). supabaseSystem mantido apenas para HIBP fire-and-forget (Cat. 2) | |
set-password | ✅ 100% migrado para RPCs (Fase 4 — 2026-04-07). Zero supabaseSystem. | |
admin-feature-flags | ✅ Migrado para RLS (Fase 1 — 2026-04-07) | |
admin-dashboard | MÉDIA | Queries em portal_rate_metrics, escolas |
admin-usuarios | MÉDIA | Queries em usuario_papeis, usuarios |
admin-usuarios-escola | MÉDIA | CRUD em papeis, usuario_papeis, usuarios |
Funções limpas na Fase 0 (dead code removido — 2026-04-07)
| Arquivo | O que foi feito |
|---|---|
especialista-olimpiadas | Removido import/instanciação morta de createSupabaseSystem |
especialista-headers | Idem |
especialista-cursos | Idem |
escola-dashboard | Idem |
eventos-calendario | Idem |
eventos-calendario-trial | Idem |
Impacto: As policies RLS dessas tabelas nunca são testadas em produção — existem mas são bypassadas.
Migração requer (projeto separado):
- Criar/validar policies RLS para cada tabela envolvida
- Testar cada function contra as policies
- Deploy gradual por módulo
Referências
supabase/functions/_shared/supabase-client.ts— Definição dos 5 construtoresdocs/security/RLS_POLICIES.md— Policies por tabela e papeldocs/security/RLS_DESIGN_GUIDE.md— Guia de design de RLS- Migration
20260406221825— Fix de privilege escalation emsub_permissoes - Migration
20260406_feature_flags_select— SELECT policies para feature flags - Migration
20260406— Fixusuario_papeis+ policiescoordenador_cores