Skip to content

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:

  1. Rollback rápido: desligar a flag desativa a feature em produção sem deploy.
  2. 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)

RegraExemplo corretoExemplo errado
snake_case, sem hífensheader_novidadesheader-novidades
Sub-flag por dot-notationmural.noticias, alunos.listamural_noticias
Prefixo do papel quando exclusivoadmin_feature_flagsfeature-flags-admin
Singular para feature, plural para coleçãoformacao, alunosmisto

Categoria (feature_flags.categoria)

Coluna física da tabela (não inferida). Valores canônicos:

CategoriaAplicação
adminPainel administrativo OLP
especialistaPainel do especialista (banners, headers, templates)
escolaGestor escolar (pagamentos, configs, gestão)
coordenadorCoordenador (controle, mural, resultados)
diretorDiretor (painéis analíticos)
portalMural Olímpico (portal aluno/responsável)
outrosQuando nenhuma categoria acima se aplica

Restringido por CHECK constraint em public.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)
EstadoCritérioativa_globalCanary
planejadaFlag criada, gate backend já em código, frontend referenciafalsenenhum
canary1+ canary_groups ativos vinculados, escolas/usuários pilotofalse≥1 grupo
globalLiberada para todostruemantido para rollback
estável≥90 dias global sem incidentetruecandidata a remoção
aposentadaGates removidos do código, flag deletada do banco

Regras:

  • Nunca pular de planejada direto para global. Sempre passar por canary se 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

ts
// 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 (helper FeatureBlockedError.toResponse()).

Frontend

tsx
import { FeatureGate } from "@/components/feature-gate";

<FeatureGate flag="minha_feature">
  <MeuComponente />
</FeatureGate>

ou para abas:

tsx
const tabs = useVisibleTabs([
  { key: "minha_feature", label: "Minha feature" },
]);

5. Cobertura de Testes Mínima

Por flag nova, são obrigatórios:

TesteCamadaCenário
Backend gate OFFDeno (*.test.ts)Flag desligada → resposta 403 FEATURE_DISABLED
Backend gate ONDenoFlag ligada → 200 + payload esperado
Frontend gateVitest + RTLComponente renderiza só com flag ativa; admin sempre vê
CanaryDeno ou hookUsuá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.
  • requireFeatureFlag apenas em update/create. list_active sem gate vaza estado oculto.
  • categoria derivada 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: cursos vs formacao). Consolidar em um nome canônico.
  • Gate referenciando flag inexistente. Fail-close gera 403 perpétuo até alguém criar a flag — registrar a flag na mesma entrega que o gate.
  • Promover de planejada direto a global sem canary, exceto em correção urgente de bug.

7. Workflow

Ao criar (Cenário A — feature nova)

  1. 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.
  2. Fase 2 — Testes: escrever os 4 testes obrigatórios da §5 antes da implementação.
  3. Fase 3 — Implementação: gate backend + frontend simétricos.
  4. Fase 4 — Auditoria: @audit §17 sem 🔴.

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.

FaseStatusEntregá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)cursosformacao_publicacao (especialista cria), formacaoformacao_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⏳ BacklogUI 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:

PapelChave canônicaCategoriaEdge function
Especialista (publica)formacao_publicacaoespecialistaespecialista-cursos
Coordenador (consome)formacao_consumocoordenadorcoordenador-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 groups
  • docs/development/DEV_WORKFLOW.md §7 — Disciplina aplicada aos cenários A/B
  • docs/development/AUDIT.md §17 — Checklist de auditoria
  • supabase/functions/_shared/feature-gate.ts — Implementação do middleware
  • src/components/feature-gate.tsx + src/hooks/useFeatureFlags.ts — Gates frontend
  • scripts/audit/feature-flag-coverage.ts — Verificação automatizada de cobertura