Feature Flags — Ciclo de Vida e Disciplina (SSOT)
Status: Mandatório Versão: 1.0 Última atualização: 2026-04-22 Aplicação: Toda feature nova ou atualização que mude contrato/comportamento observável.
Este documento é a fonte única da verdade sobre como o projeto governa o ciclo de vida de feature flags. É referenciado pelo @workflow (Cenário A/B), @audit (§17) e pelas memórias de arquitetura.
1. Princípio Central
Toda feature nasce atrás de uma flag, com gates simétricos backend + frontend.
Uma flag não é "label da UI" nem "switch decorativo". É um mecanismo de release control com dois propósitos não-negociáveis:
- Rollback rápido: desligar a flag desativa a feature em produção sem deploy.
- Rollout gradual: canary → global, sem precisar de branch-per-tenant.
Sem gate backend, a flag é bypassável por chamada direta a invokeAction/invokeEdge. Sem gate frontend, o usuário enxerga UI sem dados. Os dois lados são obrigatórios.
2. Convenção de Nomenclatura
Chave (feature_flags.chave)
| Regra | Exemplo correto | Exemplo errado |
|---|---|---|
snake_case, sem hífens | header_novidades | header-novidades |
Sub-flag por dot-notation | mural.noticias, alunos.lista | mural_noticias |
| Prefixo do papel quando exclusivo | admin_feature_flags | feature-flags-admin |
| Singular para feature, plural para coleção | formacao, alunos | misto |
Categoria (feature_flags.categoria)
Coluna física da tabela (não inferida). Valores canônicos:
| Categoria | Aplicação |
|---|---|
admin | Painel administrativo OLP |
especialista | Painel do especialista (banners, headers, templates) |
escola | Gestor escolar (pagamentos, configs, gestão) |
coordenador | Coordenador (controle, mural, resultados) |
diretor | Diretor (painéis analíticos) |
portal | Mural Olímpico (portal aluno/responsável) |
outros | Quando nenhuma categoria acima se aplica |
Restringido por
CHECKconstraint empublic.feature_flags. Sub-flags herdam a categoria da flag-pai.
parent_chave
Obrigatório em sub-flags. A chave-pai tem parent_chave = NULL.
3. Ciclo de Vida (5 estados)
planejada ──► canary ──► global ──► estável ──► aposentada
│ │ │ │
└───────────┴──────────┴───────────┘
(rollback a qualquer estado)| Estado | Critério | ativa_global | Canary |
|---|---|---|---|
| planejada | Flag criada, gate backend já em código, frontend referencia | false | nenhum |
| canary | 1+ canary_groups ativos vinculados, escolas/usuários piloto | false | ≥1 grupo |
| global | Liberada para todos | true | mantido para rollback |
| estável | ≥90 dias global sem incidente | true | candidata a remoção |
| aposentada | Gates removidos do código, flag deletada do banco | — | — |
Regras:
- Nunca pular de
planejadadireto paraglobal. Sempre passar porcanaryse a feature toca ≥1 papel ou tem risco de regressão. - Ao chegar em
aposentada: remoção do gate e do registro do banco na mesma entrega. Nunca deixar gate sem flag (fail-close = 403 perpétuo) nem flag sem gate (lixo de configuração).
4. Cobertura Simétrica Obrigatória
Backend
// supabase/functions/<feature>/index.ts
import { requireFeatureFlag } from "../_shared/feature-gate.ts";
const user = await requireRole(req, "coordenador", "minha-feature");
await requireFeatureFlag(supabase, user, "minha_feature", req);
// ...handler...Regras:
- Gate em todas as ações da edge function, incluindo leitura (
list,get_*,list_active). - Admin tem bypass automático (
BYPASS_ROLES = ["administrador"]). - Erro retornado:
403 FEATURE_DISABLED(helperFeatureBlockedError.toResponse()).
Frontend
import { FeatureGate } from "@/components/feature-gate";
<FeatureGate flag="minha_feature">
<MeuComponente />
</FeatureGate>ou para abas:
const tabs = useVisibleTabs([
{ key: "minha_feature", label: "Minha feature" },
]);5. Cobertura de Testes Mínima
Por flag nova, são obrigatórios:
| Teste | Camada | Cenário |
|---|---|---|
| Backend gate OFF | Deno (*.test.ts) | Flag desligada → resposta 403 FEATURE_DISABLED |
| Backend gate ON | Deno | Flag ligada → 200 + payload esperado |
| Frontend gate | Vitest + RTL | Componente renderiza só com flag ativa; admin sempre vê |
| Canary | Deno ou hook | Usuário em canary group + flag global OFF → acesso permitido |
Referência: supabase/functions/_shared/__tests__/feature-gate.test.ts.
6. Anti-padrões (proibidos)
- ❌ Flag só no frontend. UI esconde mas backend serve dados.
- ❌
requireFeatureFlagapenas emupdate/create.list_activesem gate vaza estado oculto. - ❌
categoriaderivada por heurística no backend. Ler do banco; backfill já ocorreu. - ❌ Chave em kebab-case. Use snake. Migrar com alias se for legado.
- ❌ Duplicação semântica (ex:
cursosvsformacao). Consolidar em um nome canônico. - ❌ Gate referenciando flag inexistente. Fail-close gera
403perpétuo até alguém criar a flag — registrar a flag na mesma entrega que o gate. - ❌ Promover de
planejadadireto aglobalsem canary, exceto em correção urgente de bug.
7. Workflow
Ao criar (Cenário A — feature nova)
- Fase 1 — Planejamento: declarar nome da flag, categoria, plano de rollout (canary inicial), mapa de gates (lista de edge functions + componentes), critério de promoção a global, plano de aposentadoria.
- Fase 2 — Testes: escrever os 4 testes obrigatórios da §5 antes da implementação.
- Fase 3 — Implementação: gate backend + frontend simétricos.
- Fase 4 — Auditoria:
@audit §17sem 🔴.
Ao atualizar (Cenário B — feature existente)
Na Fase 1 (Raio-X), responder:
- A feature já tem flag? Em qual estado do ciclo?
- Backend tem gate em todas as ações?
- Frontend tem gate?
- Categoria é a correta no banco?
- Chave segue convenção atual? Se não, planejar migração com alias.
- Existe duplicação semântica?
Se algo desviar: corrigir na Fase 2 (baseline) antes de evoluir a feature.
8. Migração Incremental (estado deste projeto)
A adoção desta disciplina segue o mesmo princípio de migração incremental do @workflow §5: toda feature tocada sai sob a nova disciplina, sem big bang.
| Fase | Status | Entregável |
|---|---|---|
F1 — Schema categoria | ✅ Concluída (Abr/2026) | Coluna física com backfill |
F2 — Backend lê categoria real | ✅ Concluída (Abr/2026) | admin-feature-flags.list para de inferir |
| F3 — Convenção snake_case | ✅ Concluída (Abr/2026) | Migração completa de kebab→snake (sem alias, sistema único produto) |
| F4 — Gate backend simétrico | ✅ Concluída (Abr/2026) | requireFeatureFlag em todos admin-* + gate público em especialista-headers |
F5 — Segregação cursos/formacao | ✅ Concluída (Abr/2026) | cursos → formacao_publicacao (especialista cria), formacao → formacao_consumo (coordenador consome) |
F5b — Segregação header_novidades | ✅ Concluída (Abr/2026) | header_novidades (gestão pelo especialista) + header_novidades_consumo (carrossel para escola/coord/diretor). Inclui mudança de semântica HTTP: pausa intencional → 200, falha real → 503. |
| F6 — Aposentadoria assistida | ⏳ Backlog | UI sugere flags global há >90d sem incidente |
Case-study: Domínio "Formação" (segregação publicador/consumidor)
A feature Formação é compartilhada entre dois papéis com objetivos opostos:
- Especialista — papel CRIADOR/PUBLICADOR. Alimenta a plataforma adicionando cursos e vídeos.
- Coordenador — papel CONSUMIDOR. Apenas assiste o conteúdo já publicado.
São fluxos independentes e devem poder ser pausados/liberados separadamente (ex: pausar publicação durante refatoração do CMS sem afetar o consumo dos coordenadores). Por isso o sistema utiliza chaves segregadas com sufixo semântico:
| Papel | Chave canônica | Categoria | Edge function |
|---|---|---|---|
| Especialista (publica) | formacao_publicacao | especialista | especialista-cursos |
| Coordenador (consome) | formacao_consumo | coordenador | coordenador-videos |
Regra geral: domínios compartilhados entre papéis com responsabilidades distintas (CRUD vs leitura, admin vs end-user) DEVEM ter chaves separadas. Sufixos canônicos: _publicacao / _consumo (ou equivalente semântico no domínio).
9. Referências Cruzadas
docs/architecture/CANARY_RELEASE_SYSTEM.md— Infraestrutura de canary groupsdocs/development/DEV_WORKFLOW.md §7— Disciplina aplicada aos cenários A/Bdocs/development/AUDIT.md §17— Checklist de auditoriasupabase/functions/_shared/feature-gate.ts— Implementação do middlewaresrc/components/feature-gate.tsx+src/hooks/useFeatureFlags.ts— Gates frontendscripts/audit/feature-flag-coverage.ts— Verificação automatizada de cobertura