Catalisa.
Building blocks/PlataformaProdução

Feature Flags

Ligue e desligue funcionalidade em produção sem novo deploy, por segmento de cliente

25
Endpoints
6
Entidades
0
Provedores
Tenant
Escopo
3010
Porta

Você para de escolher entre "sobe para todo mundo" e "não sobe". A funcionalidade vai para produção desligada, você liga para 5% dos clientes, olha o resultado e desliga em um clique se der errado — sem esperar a janela de deploy.

Para quem é
  • Fintechs e financeiras que precisam liberar mudança de esteira de crédito por cliente, não para a base inteira
  • Plataformas B2B que vendem para empresas com contratos diferentes e precisam ligar funcionalidade por contrato
  • Times de produto que fazem deploy diário e não podem transformar cada release em evento de risco
Substitui
  • Assinatura de uma plataforma de feature management por assento ou por usuário ativo
  • Tabela `configuracoes` caseira com booleano por cliente, lida direto do banco em cada requisição
  • Branch de longa duração esperando a funcionalidade ficar "pronta o suficiente" para merge
O que não é
  • Plataforma de experimentação A/B com análise estatística (não calcula significância nem lift — ver §15)
  • Sistema de configuração de aplicação ou cofre de segredos
  • Ferramenta de deploy ou CI/CD

01Resumo executivo

O Feature Flags separa duas decisões que quase todo time trata como uma só: subir o código e ligar a funcionalidade. O código sobe desligado; ligar vira uma chamada de API, e desligar também.

Na prática, uma financeira que muda a política de análise de proposta não precisa mais escolher entre "vai para os 120 correspondentes de uma vez" e "não vai". Ela liga a política nova para três correspondentes, olha a taxa de aprovação por uma semana, e sobe para 100%. Se der errado às três da tarde de uma sexta, o caminho de volta é POST /disable — não é um rollback de deploy.

Está em produção desde novembro de 2025, roda tenant-scoped sobre o token do IAM e usa o Decision Engine quando a regra de segmentação é complexa demais para uma lista de condições.

AtributoValor
Identificadorfeature-flags
CategoriaPlataforma
EscopoTenant (exige organizationId no token)
Porta (standalone)3010
Path alias@feature-flags
Prefixo HTTP/feature-flags
Schema no bancoflags
StatusProdução desde 2025-11
Depende dePostgreSQL, Redis, IAM, Decision Engine (só para segmento DMN)

02O problemanegócio

O cenário. Um time entrega software para várias empresas clientes na mesma plataforma. Cada cliente tem um apetite de risco diferente, um contrato diferente e um calendário diferente. Toda mudança relevante precisa de uma resposta para "e se quebrar para o cliente grande?".

O que trava hoje.

  • Deploy e release são a mesma decisão. Subir o código significa ligar para todo mundo. Então a barra para subir fica alta, o lote fica grande, e lote grande é exatamente o que aumenta a chance de quebrar.
  • A branch de longa duração apodrece. Enquanto a funcionalidade não está "pronta o suficiente", ela vive fora da main. Quando finalmente entra, entra com três semanas de divergência e um merge que ninguém quer revisar.
  • O rollback é caro e lento. Reverter um deploy exige pipeline, aprovação e janela. Nesse tempo o cliente está com a funcionalidade quebrada.
  • A liberação por cliente vira if no código. Alguém escreve if (organizationId === '...'), o próximo escreve em outro lugar, e seis meses depois ninguém sabe quais clientes têm o quê. Descobrir exige grep.
  • As plataformas do mercado cobram pela dimensão errada. Preço por assento penaliza time grande; preço por usuário ativo mensal penaliza o cliente que cresce. Nenhuma das duas dimensões tem relação com o valor que a flag entrega.

O custo de não resolver. Pete Hodgson descreve o mecanismo com precisão na referência clássica sobre o assunto: os release toggles existem para implementar o princípio de entrega contínua de "separar a liberação da funcionalidade do deploy do código", permitindo que código incompleto vá para produção como caminho latente que pode nunca ser ligado (Martin Fowler — *Feature Toggles*). Sem isso, o custo aparece como lote grande, janela de deploy noturna e rollback caro — e cada um desses é uma escolha que o time faz porque não tem o interruptor, não porque queira.


03Proposta de valornegócio

AntesDepois
Subir código = ligar funcionalidade para todosSobe desligado; ligar é POST /:flagId/enable
Rollback é reverter deploy e esperar o pipelineRollback é desligar a flag, com efeito em até 5 minutos
Liberação por cliente é if espalhado no códigoSegmento nomeado, versionado e consultável por API
"Vamos testar com poucos" não tem como ser feitoRollout percentual com bucketing consistente por usuário
Regra de segmentação complexa vira códigoTabela DMN executada pelo Decision Engine

O interruptor é de operação, não de deploy. Ligar e desligar exige a permissão FEATURE_FLAGS_TOGGLE, separada de FEATURE_FLAGS_UPDATE. Quem opera pode desligar em incidente sem poder alterar a definição da flag.

O mesmo usuário sempre vê o mesmo lado. O rollout percentual usa hash consistente de userId:flagKey (murmurhash v3). Um usuário em 10% continua em 10% quando você sobe para 20% — a experiência não pisca a cada requisição.

A regra de segmentação pode ser a sua regra de negócio. Um segmento pode ser uma lista de condições sobre atributos (renda > 5000 AND uf in [SP, RJ]) ou uma tabela DMN completa, executada pelo Decision Engine. Quem escreve a política de crédito consegue escrever quem vê a funcionalidade nova, sem passar por um deploy.

Flag não é booleano. Uma variação carrega qualquer JSON — booleano, string, número, objeto ou lista. Dá para usar a mesma flag para ligar a funcionalidade e para carregar o parâmetro dela (o limite, o texto, a lista de bancos habilitados).

O custo não sobe com o seu cliente. A cobrança é por avaliação e por flag ativa, não por assento nem por usuário ativo mensal.


04Casos de uso reaisnegócio

Caso 1 — Uma financeira muda a política de crédito para 3 correspondentes antes de mudar para 120 Cenário ilustrativo

Contexto. Financeira de consignado com 120 correspondentes bancários. A esteira roda um conjunto de regras de aceite que define quem passa para análise humana.

A dor. Mudar o corte de score era um evento. Ia para todos os correspondentes ao mesmo tempo, porque não havia como fazer diferente. Quando o corte novo derrubava a taxa de aprovação de um perfil que ninguém tinha previsto, a descoberta acontecia dois dias depois, no relatório — e o conserto era outro deploy.

A solução com o BB. A esteira passa a consultar POST /feature-flags/api/v1/evaluate/politica-aceite-v2 mandando context.attributes com o correspondente e o perfil da proposta. A flag tem duas variações (v1 e v2) e uma regra de targeting apontando para um segmento SIMPLE com a condição correspondenteId in [...]. Subir de 3 para 120 é um PATCH na regra.

O resultado. A mudança de política deixa de ser um evento de deploy e vira um dial. E como o resultado de cada avaliação é registrado por amostragem (10%), a comparação entre v1 e v2 sai do próprio banco em vez de sair de uma planilha.

Caso 2 — Um kill switch que o plantão consegue acionar sem pedir deploy Cenário ilustrativo

Contexto. Plataforma de pagamentos que integra com um provedor externo de antifraude. O provedor tem incidente de latência algumas vezes por ano.

A dor. Quando o provedor degradava, a fila de transações travava atrás de um fetch com timeout de 8 segundos. A única saída era um deploy com a integração comentada — 25 minutos no melhor caso, com o time de plantão escrevendo código às duas da manhã.

A solução com o BB. A chamada ao provedor passa a ser guardada por uma flag antifraude-externo-ativo, avaliada a cada transação com cache de 5 minutos. O plantão tem a permissão FEATURE_FLAGS_TOGGLE e nada mais — não consegue alterar variação, criar flag nem mexer em segmento.

O resultado. O tempo entre "identificamos o provedor degradado" e "paramos de chamar o provedor" cai de um ciclo de deploy para uma chamada de API mais o TTL do cache. E a ação fica registrada: o evento feature-flags.flag.disabled é publicado com o usuário que desligou.

Caso 3 — Liberação contratual por cliente, sem if no código Cenário ilustrativo

Contexto. Plataforma B2B de seguros que vende módulos separados. Nem todo cliente contrata o módulo de sinistro digital.

A dor. A liberação por contrato estava espalhada: um if no backend, uma variável de ambiente no frontend e uma coluna booleana numa tabela de configuração. Quando o comercial fechava um contrato novo, ligar o módulo exigia três mudanças coordenadas, e uma delas sempre era esquecida.

A solução com o BB. Um segmento clientes-com-sinistro-digital e uma flag modulo-sinistro-digital com variação booleana. O frontend chama POST /feature-flags/api/v1/evaluate/modulo-sinistro-digital no bootstrap; o backend avalia a mesma flag antes de expor a rota. Uma fonte de verdade, dois consumidores.

O resultado. Ligar um cliente novo vira um PATCH no segmento. E responder "quais clientes têm o módulo?" vira GET no segmento, em vez de uma reunião.

Caso 4 — A separação entre deploy e release como prática estabelecida Referência de mercado

Contexto. A taxonomia de referência de feature toggles distingue quatro categorias: release toggles (código incompleto em produção), experiment toggles (A/B), ops toggles (degradar funcionalidade em produção) e permissioning toggles (liberar por tipo de usuário) — Martin Fowler / Pete Hodgson, *Feature Toggles*.

A dor do mercado. O mesmo material é explícito sobre o outro lado: "toggles vêm com um custo de manutenção", e times experientes tratam as flags no código como estoque, que carrega custo, e buscam manter esse estoque baixo. Flag que fica ligada em 100% por dois anos é dívida técnica com aparência de funcionalidade.

Como a Catalisa endereça. As três primeiras categorias são atendidas hoje: release toggle (enable/disable), ops toggle (permissão FEATURE_FLAGS_TOGGLE separada) e permissioning toggle (segmento por atributo). A quarta — experiment toggle com análise estatística — não é atendida: o BB roteia o usuário de forma consistente e registra a avaliação, mas não calcula significância (ver §15). Sobre a dívida de toggle, o BB não remove flag sozinho; a limpeza continua sendo disciplina do time, e GET /flags?filter[status]=ENABLED é a ferramenta para encontrar candidatas.

O resultado. Uma implementação honesta de três das quatro categorias, com a quarta claramente marcada como fora de escopo em vez de insinuada.


05Mercado e diferenciaisnegócio

Panorama. O mercado de feature management se organizou em torno de duas perguntas: por qual dimensão cobrar e onde a flag é avaliada. LaunchDarkly cobra por consumo (conexões de serviço e MAU client-side) e avalia no SDK com streaming. Unleash e GrowthBook cobram por assento e são open source, com avaliação local. Flagsmith e ConfigCat cobram por requisição ou por download de configuração. Todas resolvem bem o caso "SaaS B2C com muitos usuários finais".

Nenhuma delas conhece o seu domínio. Quando a regra de "quem vê" depende de política de crédito, de tipo de contrato ou de uma matriz de decisão do time de risco, todas te devolvem o mesmo primitivo: uma lista de condições sobre atributos que você preenche. O Feature Flags da Catalisa aceita esse mesmo primitivo — e aceita também uma tabela DMN, avaliada pelo Decision Engine que já roda ao lado.

CritérioCatalisa Feature FlagsLaunchDarklyGrowthBookFlagsmithUnleash
Dimensão de cobrançaAvaliação e flag ativaConexão de serviço + MAUAssentoRequisição de APIAssento
Preço público de entradaEm definiçãoUS$ 0 (Developer)US$ 0 (self-host)US$ 0 (50k req/mês)US$ 75/assento/mês
Variação multivalorada (JSON)SimSimSimSimSim
Rollout percentual consistenteSim (murmurhash v3)SimSimSimSim
Segmentação por atributoSim (11 operadores)SimSimSimSim
Regra de segmentação em DMNSimNãoNãoNãoNão
Experimentação A/B com estatísticaNão (ver §15)SimSim, forteParcialParcial
SDKs oficiaisNão — API HTTP (ver §15)Sim, extensoSimSimSim
Ambientes (dev/homolog/prod)Não (ver §15)SimSimSimSim
Avaliação sem chamada de redeNãoSim (streaming)SimSimSim
Multi-tenant com isolamento por tokenSim, do IAMVia projeto/ambienteVia organizaçãoVia projetoVia projeto
Self-hostSim (é seu deploy)NãoSimSimSim

Preços de tabela pública consultados em 2026-08-16: LaunchDarkly, GrowthBook, Flagsmith, Unleash, ConfigCat. Tabelas mudam; confira na data da sua análise.

Nossos diferenciais

  1. A segmentação pode ser uma tabela de decisão. Um Segment do tipo DMN carrega XML de DMN e é avaliado pelo Decision Engine via facade. Isso é difícil de copiar não pela técnica, mas porque exige ter um motor DMN operado ao lado — o que um fornecedor de feature flags puro não tem motivo para construir.
  2. O tenant não é convenção. O organizationId vem do JWT assinado pelo IAM, e todos os 25 endpoints aplicam requireOrganization. Uma flag de um cliente não é visível para outro porque a consulta ao banco nunca recebe o tenant do corpo da requisição.
  3. A cobrança não pune crescimento. Assento penaliza time grande; MAU penaliza cliente que cresce. Avaliação e flag ativa são as duas dimensões que de fato acompanham o valor entregue.

Quando escolher o concorrente. Se você precisa de experimentação A/B de verdade — significância estatística, CUPED, teste sequencial, análise sobre o data warehouse — o GrowthBook faz isso hoje e nós não fazemos (§15). Se o seu produto é B2C com milhões de usuários finais e você precisa de avaliação sem chamada de rede, com SDK que mantém as flags em memória e recebe atualização por streaming, o LaunchDarkly e o Unleash resolvem isso e nós ainda exigimos uma chamada HTTP por avaliação (mitigada por cache de 5 minutos). Se você quer ambientes separados (dev/homologação/produção) com promoção de flag entre eles, todos os cinco concorrentes têm e nós não temos. O Feature Flags ganha quando o problema é liberação controlada por cliente empresarial dentro de uma plataforma multi-tenant, especialmente quando a regra de "quem vê" é regra de negócio de verdade — não quando o problema é otimizar conversão de um funil B2C.


06Modelo de cobrança e ROInegócio

Unidade de cobrança. Precificação em definição. Os drivers estão definidos e são estes:

DriverPor que importa
Volume de avaliações de flagÉ a chamada quente; cada uma consulta cache ou banco
Número de flags ativasEstoque de configuração mantida e cacheada
Número de segmentos com regra DMNCada avaliação DMN é uma chamada ao Decision Engine, e custa mais que uma condição em memória

O que dispara custo, na prática. Uma avaliação com cache quente custa uma leitura no Redis. Uma avaliação de segmento SIMPLE custa a mesma leitura mais aritmética em memória. Uma avaliação de segmento DMN custa uma chamada ao Decision Engine — por isso a associação (organização, segmento, usuário) é cacheada por 15 minutos.

Comparação de custo — cenário nomeado: plataforma B2B com 12 pessoas mexendo em flags, 80 empresas clientes, cerca de 40 mil usuários finais ativos por mês e 6 serviços consultando flags.

CatalisaLaunchDarkly (Foundation)Unleash (PAYG)GrowthBook (Pro)
Base de cálculoAvaliações + flags ativasUS$ 10 por Service Connection + US$ 8,33 por 1k MAU client-sideUS$ 75 por assento, mínimo 5US$ 40 por assento
Conta do cenárioEm definição6 conexões + 40 mil MAU12 assentos12 assentos
Ordem de grandeza mensalCentenas de dólares~US$ 900~US$ 480
Sobe se o cliente crescer?Só com o volume de avaliaçõesSim, com MAUNãoNão
Sobe se o time crescer?NãoNãoSimSim

Estimativa a partir das tabelas públicas consultadas em 2026-08-16, aplicando a conta direta do plano de entrada. Não é proposta comercial: descontos anuais, negociação e mudança de tabela alteram o resultado. A coluna Catalisa está em definição e não deve ser apresentada com número.

ROI. O retorno não está na linha de licença — GrowthBook self-hosted tem custo de licença zero. Está em dois lugares:

  • O incidente que não acontece. Cada release que sobe para 5% antes de 100% é uma chance de descobrir o problema com 5% do impacto. Numa operação de crédito, a diferença entre "propostas de 120 correspondentes com política errada" e "propostas de 3 correspondentes" é a diferença entre um incidente e um ajuste.
  • O if que não é escrito. Cada liberação por cliente que vira segmento em vez de condicional no código é uma linha que não precisa de deploy para mudar, e um lugar a menos para procurar quando alguém pergunta quem tem o quê.

07Arquitetura

                    HTTP
                      │
  ┌───────────────────┴─────────────────────────────────────────────────┐
  │ Hono app  basePath('/feature-flags')                                 │
  │                                                                      │
  │  /api/v1/flags                          flagsRouter          (7)     │
  │  /api/v1/flags/:flagId/targeting-rules  targetingRulesRouter (6)     │
  │  /api/v1/flags/:flagId/overrides        overridesRouter      (4)     │
  │  /api/v1/segments                       segmentsRouter       (5)     │
  │  /api/v1/evaluate                       evaluateRouter       (3)     │
  │  /health                                                             │
  └───────────────────┬─────────────────────────────────────────────────┘
                      │  authMiddleware → requirePermission → requireOrganization
                      │  Zod parse → ResultAsync<T, AppError>
  ┌───────────────────┴─────────────────────────────────────────────────┐
  │ services/                                                            │
  │   FlagService               CRUD, enable/disable, valida variações   │
  │   SegmentService            CRUD de segmentos                        │
  │   EvaluationService         precedência, cache, log amostrado        │
  │   SegmentMembershipService  SIMPLE em memória · DMN via facade       │
  └──────┬──────────────────────────────┬──────────────────────┬────────┘
         │                              │                      │
  ┌──────┴────────┐        ┌────────────┴──────────┐   ┌───────┴────────────┐
  │ repositories/ │        │ Redis                 │   │ DecisionEngine     │
  │ Prisma        │        │  ff:flag:{org}:{key}  │   │ Facade             │
  │ schema flags  │        │        TTL 300s       │   │  local  = TypeDI   │
  └───────────────┘        │  ff:membership:       │   │  remote = HTTP     │
                           │   {org}:{seg}:{user}  │   └────────────────────┘
                           │        TTL 900s       │
                           └───────────────────────┘

Precedência de avaliação — a ordem importa e é fixa:

  POST /feature-flags/api/v1/evaluate/:flagKey  { context: { userId, attributes } }
                      │
                      ▼
        ┌─────────────────────────────┐
        │ 1. flag existe?             │──── não ──▶ 404 NOT_FOUND
        └──────────────┬──────────────┘
                       ▼
        ┌─────────────────────────────┐
        │ 2. status == DISABLED?      │──── sim ──▶ variação padrão   reason=DISABLED
        └──────────────┬──────────────┘
                       ▼
        ┌─────────────────────────────┐
        │ 3. override para o userId?  │──── sim ──▶ variação do override  reason=OVERRIDE
        └──────────────┬──────────────┘
                       ▼
        ┌─────────────────────────────┐
        │ 4. regras, priority ASC     │
        │    (menor priority primeiro)│
        │    ├ todos os segmentos     │
        │    │  casam? (lógica AND)   │──── não ──▶ próxima regra
        │    └ rollout% cobre o user? │──── não ──▶ próxima regra
        └──────────────┬──────────────┘
                       │ sim
                       ▼            variação da regra   reason=RULE_MATCH
        ┌─────────────────────────────┐
        │ 5. nenhuma regra casou      │──────────▶ variação padrão   reason=DEFAULT
        └─────────────────────────────┘

Decisões não óbvias.

  • Flag nasce DISABLED. FlagService.createFlag força status: 'DISABLED' mesmo que o corpo peça outra coisa. Criar não pode ser o mesmo ato de ligar — senão o primeiro POST de um script de setup vira um release não intencional.
  • Cache de 300s na flag, 900s na associação de segmento. A flag muda com frequência (é o ponto dela), então 5 minutos é o teto tolerado entre desligar e o efeito aparecer, e toda mutação invalida a chave explicitamente. A associação de um usuário a um segmento muda pouco e custa caro quando é DMN (chamada de rede ao Decision Engine), então 15 minutos compensa. O trade-off aceito: mudar a regra de um segmento leva até 15 minutos para valer para quem já foi avaliado, mesmo com a invalidação por padrão de chave.
  • Bucketing por murmurhash.v3(userId:flagKey) % 100. Hash não criptográfico, rápido e uniforme o suficiente para 100 baldes. O flagKey entra no hash de propósito: sem ele, o mesmo usuário cairia sempre no mesmo balde em todas as flags, e um usuário azarado ficaria fora de todo rollout de 10% da plataforma para sempre.
  • Sem userId no contexto, regra com rollout < 100% é pulada. Não há como fazer bucketing consistente sem identificador. A alternativa — sortear — daria resposta diferente a cada requisição para o mesmo usuário, que é o pior comportamento possível numa flag. Pular é conservador e previsível.
  • Log de avaliação com amostragem de 10%, fire-and-forget. Registrar 100% transformaria uma leitura de cache num INSERT. A amostragem preserva a distribuição por variação e por motivo, que é o que importa para diagnóstico, sem transformar o caminho quente em escrita.
  • evaluate-batch e evaluate-all não usam o cache de flag. Elas buscam direto no banco (findManyByKeys, findAllEnabled), porque o cache é indexado por chave individual. São endpoints de bootstrap, chamados uma vez por sessão — não são o caminho quente.
  • Segmentos de uma regra são combinados com AND. Uma regra com dois segmentos exige pertencer aos dois. Para OR, crie duas regras com a mesma variação, ou use operator: "OR" dentro das condições de um único segmento SIMPLE.

Monolito vs. standalone. Em monolito, o SegmentMembershipService alcança o Decision Engine por chamada direta no container TypeDI. Em standalone — o modo usado em produção — a mesma interface IDecisionEngineFacade faz HTTP para MODULE_DECISION_ENGINE_URL. O serviço de feature flags não sabe a diferença. Nos dois modos o BB expõe apenas HTTP: não há consumidor de fila nem job agendado no main.ts.


08Conceitos e modelo de dados

Glossário

TermoSignifica
FlagA funcionalidade controlada. Tem uma key única por organização e um conjunto de variações.
Variation (variação)Um par { key, value }. O value é qualquer JSON — booleano, string, número, objeto ou lista.
Default variationA variação devolvida quando a flag está desligada ou quando nenhuma regra casa. Precisa existir na lista de variações.
Segment (segmento)Um conjunto de usuários definido por regra. Tipo SIMPLE (condições JSON) ou DMN (tabela de decisão).
Targeting rule (regra de targeting)"Se o usuário está nestes segmentos e cai neste percentual, devolva esta variação." Ordenada por priority.
OverrideFixação manual de uma variação para um userId específico. Vence qualquer regra.
Rollout percentagePercentual de usuários da regra que recebem a variação. Bucketing consistente por userId:flagKey.
Evaluation contextO que você manda na avaliação: userId (opcional) e attributes (mapa de string/número/booleano/null).
Evaluation reasonPor que a resposta foi essa: DISABLED, OVERRIDE, RULE_MATCH ou DEFAULT.

Modelo de dados — schema flags no PostgreSQL.

Modelo PrismaTabelaPropósitoCampos-chave
FeatureFlagflags.feature_flagsA flagÚnico (organizationId, key), status, variations (JSONB), defaultVariation, deletedAt
Segmentflags.segmentsConjunto de usuáriosÚnico (organizationId, key), ruleType, ruleContent, decisionId, deletedAt
FlagTargetingRuleflags.flag_targeting_rulesRegra de uma flagflagId, priority, variationKey, rolloutPercentage, inlineDmnRule
TargetingRuleSegmentflags.targeting_rule_segmentsLigação regra ↔ segmentoChave composta (ruleId, segmentId)
FlagOverrideflags.flag_overridesFixação por usuárioÚnico (flagId, userId), variationKey
FlagEvaluationLogflags.flag_evaluation_logsAvaliações amostradasflagKey, variationKey, reason, context, evaluationTimeMs

Enumerações

EnumValores
FeatureFlagStatusENABLED · DISABLED
SegmentRuleTypeSIMPLE · DMN
EvaluationReason (não é enum no banco)DISABLED · DEFAULT · OVERRIDE · RULE_MATCH

Ciclo de vida da flag

        POST /flags
             │
             ▼
      ┌────────────┐   POST /:id/enable    ┌──────────┐
      │  DISABLED  │ ────────────────────▶ │ ENABLED  │
      │ (nasce aqui│ ◀──────────────────── │          │
      │  sempre)   │   POST /:id/disable   └──────────┘
      └─────┬──────┘                             │
            │              DELETE /:id           │
            └──────────────────┬─────────────────┘
                               ▼
                      ┌──────────────────┐
                      │ deletedAt != null│  exclusão lógica: a linha fica,
                      │  (some da API)   │  a flag some das consultas e do cache
                      └──────────────────┘

  Só existem dois estados. Não há rascunho, agendamento nem arquivamento.

Operadores de condição em segmento SIMPLE

O ruleContent de um segmento SIMPLE é um JSON { operator: "AND" | "OR", conditions: [...] }. Cada condição é { attribute, operator, value }:

OperadorAplica aObservação
eq · neqQualquer tipoComparação estrita (===)
gt · gte · lt · lteNúmerosDevolve false se qualquer lado não for número
in · notInListavalue precisa ser array
contains · startsWith · endsWithStringsDevolve false se qualquer lado não for string

O atributo userId é especial: quando condition.attribute === "userId", o valor comparado é o context.userId, não context.attributes.userId.


09Referência da API

Prefixo real: /feature-flags. Em staging, a base é https://flags.bb.stg.catalisa.app.

Todos os 25 endpoints exigem authMiddleware (Bearer JWT do IAM) e requireOrganization — token sem organizationId recebe 403 antes de chegar na regra de negócio.

Flags — /feature-flags/api/v1/flags

MétodoRotaDescriçãoPermissão
POST/feature-flags/api/v1/flagsCria flag (nasce DISABLED)FEATURE_FLAGS_CREATE
GET/feature-flags/api/v1/flagsLista flags, paginado, filtro filter[status]FEATURE_FLAGS_READ
GET/feature-flags/api/v1/flags/:flagIdBusca flag por UUIDFEATURE_FLAGS_READ
PATCH/feature-flags/api/v1/flags/:flagIdAtualiza nome, descrição, variações e padrãoFEATURE_FLAGS_UPDATE
POST/feature-flags/api/v1/flags/:flagId/enableLiga a flagFEATURE_FLAGS_TOGGLE
POST/feature-flags/api/v1/flags/:flagId/disableDesliga a flagFEATURE_FLAGS_TOGGLE
DELETE/feature-flags/api/v1/flags/:flagIdExclusão lógica. Responde 204FEATURE_FLAGS_DELETE

Segmentos — /feature-flags/api/v1/segments

MétodoRotaDescriçãoPermissão
POST/feature-flags/api/v1/segmentsCria segmentoSEGMENTS_CREATE
GET/feature-flags/api/v1/segmentsLista segmentos, paginadoSEGMENTS_READ
GET/feature-flags/api/v1/segments/:segmentIdBusca segmento por UUIDSEGMENTS_READ
PATCH/feature-flags/api/v1/segments/:segmentIdAtualiza nome, descrição, ruleContent, decisionIdSEGMENTS_UPDATE
DELETE/feature-flags/api/v1/segments/:segmentIdExclusão lógica. Responde 204SEGMENTS_DELETE

Regras de targeting — /feature-flags/api/v1/flags/:flagId/targeting-rules

MétodoRotaDescriçãoPermissão
POST/feature-flags/api/v1/flags/:flagId/targeting-rulesCria regraFEATURE_FLAGS_UPDATE
GET/feature-flags/api/v1/flags/:flagId/targeting-rulesLista regras da flag, por priority ASCFEATURE_FLAGS_READ
GET/feature-flags/api/v1/flags/:flagId/targeting-rules/:ruleIdBusca uma regraFEATURE_FLAGS_READ
PATCH/feature-flags/api/v1/flags/:flagId/targeting-rules/:ruleIdAtualiza regraFEATURE_FLAGS_UPDATE
DELETE/feature-flags/api/v1/flags/:flagId/targeting-rules/:ruleIdRemove regra. Responde 204FEATURE_FLAGS_UPDATE
POST/feature-flags/api/v1/flags/:flagId/targeting-rules/reorderReordena por lista de IDsFEATURE_FLAGS_UPDATE

Overrides — /feature-flags/api/v1/flags/:flagId/overrides

MétodoRotaDescriçãoPermissão
POST/feature-flags/api/v1/flags/:flagId/overridesFixa variação para um userIdFEATURE_FLAGS_UPDATE
GET/feature-flags/api/v1/flags/:flagId/overridesLista overrides, paginadoFEATURE_FLAGS_READ
GET/feature-flags/api/v1/flags/:flagId/overrides/:overrideIdBusca um overrideFEATURE_FLAGS_READ
DELETE/feature-flags/api/v1/flags/:flagId/overrides/:overrideIdRemove override. Responde 204FEATURE_FLAGS_UPDATE

Avaliação — /feature-flags/api/v1/evaluate

MétodoRotaDescriçãoPermissão
POST/feature-flags/api/v1/evaluate/:flagKeyAvalia uma flag pela keyFEATURE_FLAGS_EVALUATE
POST/feature-flags/api/v1/evaluate/-batchAvalia até 50 flags de uma vezFEATURE_FLAGS_EVALUATE
POST/feature-flags/api/v1/evaluate/-allAvalia todas as flags ENABLED (bootstrap)FEATURE_FLAGS_EVALUATE

⚠️ -batch e -all estão registradas com um caminho que não é alcançável na prática. Os handlers são declarados como '-batch' e '-all' sem barra inicial, o que o Hono resolve para /feature-flags/api/v1/evaluate/-batch e /-all. Como POST /:flagKey é registrada antes, ela captura essas duas rotas primeiro e trata -batch como se fosse a key de uma flag — o resultado é 404 Feature flag not found. Use POST /feature-flags/api/v1/evaluate/:flagKey uma vez por flag até isso ser corrigido. Ver §15.

Saúde

MétodoRotaDescrição
GET/feature-flags/healthSonda de vida. Não exige token.

POST /feature-flags/api/v1/flags

Cria uma flag. Aceita corpo plano ou envelope JSON:API ({ "data": { "attributes": { ... } } }).

Request

{
  "key": "politica-aceite-v2",
  "name": "Política de aceite v2",
  "description": "Novo corte de score na esteira de consignado",
  "variations": [
    { "key": "v1", "value": false },
    { "key": "v2", "value": true }
  ],
  "defaultVariation": "v1"
}
CampoTipoObrigatórioRegra
keystringSim1 a 100 caracteres, apenas [a-zA-Z0-9_-]. Única por organização
namestringSim1 a 200 caracteres
descriptionstringNãoAté 1000 caracteres
variationsarraySimAo menos 1. Cada item { key (1–50), value }; value aceita booleano, string, número, objeto ou lista
defaultVariationstringSimPrecisa ser a key de uma das variações

Resposta 201

{
  "data": {
    "type": "feature-flags",
    "id": "8f2c1f6e-1c2b-4a0f-9d1a-3b7c5e9a0011",
    "links": { "self": "/api/v1/flags/8f2c1f6e-1c2b-4a0f-9d1a-3b7c5e9a0011" },
    "attributes": {
      "key": "politica-aceite-v2",
      "name": "Política de aceite v2",
      "status": "DISABLED",
      "variations": [{ "key": "v1", "value": false }, { "key": "v2", "value": true }],
      "defaultVariation": "v1"
    }
  }
}

O status volta DISABLED mesmo se você mandar outra coisa — é intencional (§7). O links.self omite o prefixo /feature-flags; trate-o como caminho relativo ao módulo, não como URL montável (§15).

Erros

StatusQuando
400Corpo reprovado no Zod, ou defaultVariation que não existe em variations
401Token ausente ou inválido
403Sem FEATURE_FLAGS_CREATE, ou token sem organizationId
409key já existe nesta organização

POST /feature-flags/api/v1/evaluate/:flagKey

O endpoint quente. O :flagKey é a key da flag, não o UUID.

Request — corpo opcional; sem corpo, o contexto é vazio.

{
  "context": {
    "userId": "cli-88213",
    "attributes": {
      "correspondenteId": "corr-014",
      "renda": 7200,
      "uf": "SP"
    }
  }
}
CampoTipoObrigatórioDescrição
context.userIdstringNãoIdentificador externo. Necessário para override e para rollout < 100%
context.attributesobjectNãoMapa de string | number | boolean | null. Aninhamento não é aceito

Resposta 200

{
  "data": {
    "flagKey": "politica-aceite-v2",
    "value": true,
    "variationKey": "v2",
    "reason": "RULE_MATCH",
    "matchedRuleId": "b1c2d3e4-0000-4000-8000-000000000001",
    "matchedSegmentIds": ["a9f0e1d2-0000-4000-8000-000000000002"]
  }
}

matchedRuleId e matchedSegmentIds só aparecem quando reason é RULE_MATCH. Use reason para diagnóstico: DEFAULT quando nenhuma regra casou, DISABLED quando a flag está desligada, OVERRIDE quando há fixação para o userId.

Erros

StatusQuando
400context fora do schema (por exemplo, atributo com objeto aninhado)
403Sem FEATURE_FLAGS_EVALUATE, ou token sem organizationId
404Flag inexistente, excluída logicamente, ou de outra organização
500defaultVariation da flag não existe na lista de variações (dado inconsistente)

POST /feature-flags/api/v1/flags/:flagId/targeting-rules

Request

{
  "name": "Piloto com 3 correspondentes",
  "priority": 0,
  "variationKey": "v2",
  "rolloutPercentage": 100,
  "segmentIds": ["a9f0e1d2-0000-4000-8000-000000000002"]
}
CampoTipoObrigatórioRegra
namestringSim1 a 200 caracteres
priorityintNão≥ 0. Menor = avaliada primeiro. Omitido, o serviço usa a próxima disponível
variationKeystringSimVariação devolvida quando a regra casa
rolloutPercentageintNão0 a 100, padrão 100. Abaixo de 100 exige context.userId
segmentIdsuuid[]NãoCombinados com AND. Lista vazia ou ausente = regra casa com todos
inlineDmnRulestringNãoAceito e persistido, mas não é avaliado hoje — ver §15

Regra sem segmentIds e com rolloutPercentage: 10 é o padrão de rollout percentual puro: casa com todo mundo e depois filtra 10% por hash.

Erros

StatusQuando
400Corpo reprovado no Zod, ou flagId que não é UUID
404Flag inexistente ou de outra organização

POST /feature-flags/api/v1/flags/:flagId/overrides

{ "userId": "cli-88213", "variationKey": "v2" }

Override vence tudo, inclusive rollout percentual. Um userId só pode ter um override por flag — o segundo devolve 409.


10Início rápido

Do zero à primeira flag ligada para 10% dos usuários. Credenciais de staging conforme AMBIENTES.md.

Os comandos abaixo não foram executados na geração deste documento. Confira as respostas contra o seu ambiente.

1. Autenticar no IAM

TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@catalisa.app",
    "password": "root123456",
    "organizationId": "b0000000-0000-0000-0000-000000000001"
  }' | jq -r .accessToken)

export FF=https://flags.bb.stg.catalisa.app/feature-flags/api/v1

2. Criar a flag

FLAG_ID=$(curl -s -X POST $FF/flags \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "key": "checkout-novo",
    "name": "Checkout novo",
    "variations": [{"key":"off","value":false},{"key":"on","value":true}],
    "defaultVariation": "off"
  }' | jq -r '.data.id')

echo "$FLAG_ID"

3. Confirmar que ela nasce desligada

curl -s -X POST $FF/evaluate/checkout-novo \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"context":{"userId":"user-1"}}' | jq
{ "data": { "flagKey": "checkout-novo", "value": false,
            "variationKey": "off", "reason": "DISABLED" } }

4. Criar a regra de rollout de 10%

curl -s -X POST $FF/flags/$FLAG_ID/targeting-rules \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Rollout 10%","priority":0,"variationKey":"on","rolloutPercentage":10}' | jq

5. Ligar a flag

curl -s -X POST $FF/flags/$FLAG_ID/enable -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'
"ENABLED"

6. Ver o rollout acontecendo

for i in $(seq 1 30); do
  curl -s -X POST $FF/evaluate/checkout-novo \
    -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
    -d "{\"context\":{\"userId\":\"user-$i\"}}" | jq -r '.data.variationKey'
done | sort | uniq -c

Espere algo perto de 3 respostas on e 27 off. Rode de novo: os mesmos usuários caem do mesmo lado, porque o balde vem de murmurhash(userId:flagKey).

7. Desligar

curl -s -X POST $FF/flags/$FLAG_ID/disable -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'

O efeito é imediato para quem consulta depois da invalidação de cache, que acontece na mesma chamada.


11Receitas

Subir um rollout de 5% para 100% com segurança

Objetivo. Aumentar a exposição em degraus sem trocar quem já está dentro.

RULE_ID=$(curl -s $FF/flags/$FLAG_ID/targeting-rules \
  -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')

for pct in 5 10 25 50 100; do
  curl -s -X PATCH $FF/flags/$FLAG_ID/targeting-rules/$RULE_ID \
    -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
    -d "{\"rolloutPercentage\": $pct}" | jq -r '.data.attributes.rolloutPercentage'
  # observe métricas antes do próximo degrau
done

Armadilhas.

  • Não mude a key da flag no meio do rollout. O balde é hash(userId:flagKey); trocar a key reembaralha todo mundo e usuários que estavam dentro saem.
  • Aumentar o percentual é seguro (o conjunto só cresce); diminuir remove usuários que já viram a funcionalidade nova. Se isso for inaceitável, desligue a flag em vez de reduzir o percentual.
  • Quem chama sem context.userId nunca entra num rollout parcial e sempre recebe a variação padrão. Confira que o seu chamador manda o identificador.

Segmentar por atributo com regra SIMPLE

Objetivo. Liberar só para clientes de SP com renda acima de 5 mil.

SEG_ID=$(curl -s -X POST $FF/segments \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "key": "sp-renda-alta",
    "name": "SP com renda acima de 5k",
    "ruleType": "SIMPLE",
    "ruleContent": "{\"operator\":\"AND\",\"conditions\":[{\"attribute\":\"uf\",\"operator\":\"eq\",\"value\":\"SP\"},{\"attribute\":\"renda\",\"operator\":\"gt\",\"value\":5000}]}"
  }' | jq -r '.data.id')

curl -s -X POST $FF/flags/$FLAG_ID/targeting-rules \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"name\":\"SP renda alta\",\"priority\":0,\"variationKey\":\"on\",\"segmentIds\":[\"$SEG_ID\"]}"

Armadilhas.

  • ruleContent é uma string com JSON dentro, não um objeto. Errar isso gera 500 na avaliação, não 400 na criação — a validação do conteúdo acontece na hora de avaliar.
  • Comparadores numéricos (gt, gte, lt, lte) devolvem false quando qualquer lado não é número. "renda": "7200" (string) nunca passa em gt: 5000.
  • O ruleType não pode ser alterado depois: o schema de PATCH de segmento não aceita o campo. Para mudar de SIMPLE para DMN, crie outro segmento.
  • Mudar ruleContent invalida o cache de associação daquele segmento, mas a propagação pode levar até 15 minutos para usuários já avaliados.

Segmentar com uma tabela DMN

Objetivo. Usar uma matriz de decisão do time de risco como critério de quem vê a funcionalidade.

curl -s -X POST $FF/segments \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "key": "elegivel-piloto",
    "name": "Elegível ao piloto (DMN)",
    "ruleType": "DMN",
    "ruleContent": "<?xml version=\"1.0\"?><definitions ...>...</definitions>"
  }'

O SegmentMembershipService manda { userId, ...attributes } como entrada da decisão e interpreta a saída assim: booleano direto; ou, se for objeto, o primeiro campo encontrado entre isMember, match e result; senão, Boolean(output).

Armadilhas.

  • Nomeie a saída da sua tabela DMN como isMember. É o primeiro nome procurado e o único que deixa a intenção explícita para quem ler a tabela depois.
  • Segmento DMN sem ruleContent sempre casa (devolve true). Um segmento DMN vazio libera para todos — é o oposto do que se espera de uma regra restritiva.
  • Cada avaliação DMN não cacheada é uma chamada de rede ao Decision Engine. Os segmentos de uma flag são avaliados em sequência, não em paralelo, de propósito: em paralelo, uma flag com cinco segmentos DMN viraria cinco chamadas simultâneas por requisição.
  • Se o Decision Engine estiver fora do ar, a avaliação da flag falha com 500 em vez de cair na variação padrão. Para caminho crítico, prefira segmento SIMPLE.

Descobrir por que um usuário está vendo a variação errada

Objetivo. Diagnóstico em quatro consultas.

# 1. O que a avaliação responde e, principalmente, por quê
curl -s -X POST $FF/evaluate/checkout-novo \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"context":{"userId":"cli-88213","attributes":{"uf":"SP","renda":7200}}}' | jq

# 2. A flag está ligada?
curl -s $FF/flags/$FLAG_ID -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'

# 3. Existe override para este usuário?
curl -s "$FF/flags/$FLAG_ID/overrides" -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | select(.attributes.userId=="cli-88213")'

# 4. Em que ordem as regras são avaliadas?
curl -s $FF/flags/$FLAG_ID/targeting-rules -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {priority: .attributes.priority, variationKey: .attributes.variationKey,
                   rollout: .attributes.rolloutPercentage, segs: .attributes.segmentIds}'

Ordem de diagnóstico, na mesma sequência da precedência: reason = DISABLED → a flag está desligada. OVERRIDE → apague o override. DEFAULT → nenhuma regra casou; confira os atributos que você mandou contra as condições do segmento. RULE_MATCH com a variação errada → confira priority: menor número ganha, e a primeira que casa encerra a avaliação.

Causa mais comum: cache. Uma mudança feita direto no banco não invalida ff:flag:{org}:{key}. Faça a mudança pela API, ou espere os 300 segundos.

Fixar uma variação para um cliente que abriu chamado

curl -s -X POST $FF/flags/$FLAG_ID/overrides \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"userId":"cli-88213","variationKey":"off"}'

Armadilhas. Override não expira. Não há campo de validade nem limpeza automática — vira dívida silenciosa. Trate a lista de overrides como fila de suporte e revise periodicamente com GET /flags/:flagId/overrides.


12Integração com outros building blocks

Building blockComo se relacionaObrigatório
IAMEmite o token; fornece organizationId e as permissões FEATURE_FLAGS_* e SEGMENTS_*Sim
Decision EngineExecuta segmentos do tipo DMN via IDecisionEngineFacadeSó para segmento DMN
Webhooks EngineEntrega os eventos feature-flags.* para sistemas do clienteNão
Audit TrailConsome os mesmos eventos para trilha de complianceNão
Decision PlatformOrquestra esteiras que consultam flags para escolher o caminhoNão
  ┌──────────┐   Bearer JWT (organizationId + permissões)   ┌──────────────────┐
  │   IAM    │ ───────────────────────────────────────────▶ │  Feature Flags   │
  └──────────┘                                              │                  │
                       segmento ruleType=DMN                │  avaliação       │
  ┌──────────────────┐ ◀───────────────────────────────────  │  precedência     │
  │ Decision Engine  │  execute(dmnXml, { userId, ...attrs })│  cache           │
  └──────────────────┘ ───────────────────────────────────▶ │                  │
                              { isMember: true }            └────────┬─────────┘
                                                                     │
                                        eventos feature-flags.*      │
                              ┌──────────────────────────────────────┴────────┐
                              ▼                                               ▼
                    ┌───────────────────┐                          ┌────────────────┐
                    │  Webhooks Engine  │ ── HTTP assinado ──▶     │  Audit Trail   │
                    │  (entrega ao      │      sistema do          │  (trilha de    │
                    │   cliente final)  │      cliente             │   compliance)  │
                    └───────────────────┘                          └────────────────┘

Este diagrama é o argumento comercial em uma imagem. Uma plataforma de feature flags avulsa para na primeira seta: ela não sabe quem é a organização sem você reimplementar o mapeamento, não tem um motor DMN ao lado para executar a sua regra de risco, e não tem para onde publicar o evento "a flag X foi desligada às 14h32 pelo usuário Y" além do próprio log dela.

Eventos publicados. Todos vão para o barramento e podem virar webhook para o cliente final:

EventoQuando
feature-flags.flag.createdFlag criada
feature-flags.flag.updatedFlag alterada
feature-flags.flag.enabledFlag ligada
feature-flags.flag.disabledFlag desligada
feature-flags.flag.deletedFlag excluída logicamente
feature-flags.segment.created · .updated · .deletedCiclo de vida do segmento
feature-flags.flag.evaluatedAvaliação amostrada (10%)

Para receber esses eventos no seu sistema, assine feature-flags.* no Webhooks Engine.


13Configuração e operação

Variáveis de ambiente

O Feature Flags não define variável própria. Ele usa as compartilhadas:

VariávelDescriçãoObrigatóriaPadrão
DATABASE_URLPostgreSQL, schema flagsSim
REDIS_URLRedis, usado nos dois cachesSim
JWT_SECRETSegredo HS256 do IAM. Mínimo 44 caracteresSim
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith
MODULE_DECISION_ENGINE_URLEndereço do Decision Engine em standalone. Necessário para segmento DMNSó em standalone com DMN
PORTPorta no modo standaloneNão3000 (mapeada para 3010 no compose)

Dependências de infraestrutura

DependênciaPara quêSe cair
PostgreSQLSchema flagsAvaliação falha com 500
RedisCache de flag (300s) e de associação de segmento (900s)Avaliação continua, direto no banco, com mais latência
IAMVerificação do token (assinatura local, sem chamada de rede)Nada muda enquanto o JWT_SECRET estiver correto
Decision EngineSegmentos DMNAvaliação da flag falha com 500 — não cai para a variação padrão

Limites e quotas

LimiteValorOnde
Tamanho da key de flag ou segmento1 a 100 caracteres, [a-zA-Z0-9_-]flagKeySchema
Tamanho da key de variação1 a 50 caracteresvariationSchema
Variações por flagMínimo 1, sem máximo declaradovariationsSchema
Flags por chamada em -batch1 a 50batchEvaluateInputSchema
Página máxima em listagens100 itenslistFlagsInputSchema, listSegmentsInputSchema
rolloutPercentageInteiro de 0 a 100createTargetingRuleInputSchema
userId de override1 a 255 caracterescreateOverrideInputSchema
TTL do cache de flag300 segundosFLAG_CACHE_TTL
TTL do cache de associação900 segundosMEMBERSHIP_CACHE_TTL
Amostragem do log de avaliação10%EVALUATION_LOG_SAMPLE_RATE

Catálogo de erros

StatusCódigoSignificaO que fazer
400VALIDATIONCorpo reprovado no Zod, defaultVariation inexistente na lista, ou parâmetro que não é UUIDConfira os tipos contra §9. :flagId é UUID; :flagKey é a key
401UNAUTHORIZEDToken ausente, inválido ou expiradoRenove o token no IAM
403FORBIDDENPermissão faltando ou token sem organizationIdConfira permissions no token; autentique informando a organização
404NOT_FOUNDFlag, segmento, regra ou override inexistente, excluído, ou de outra organizaçãoConfira o identificador. 404 também é a resposta para recurso de outro tenant — por design, para não permitir enumeração
409CONFLICTkey de flag ou segmento repetida na organização, ou override já existente para o userIdEscolha outra key, ou apague o override antes
500INTERNALdefaultVariation inconsistente, ruleContent de segmento SIMPLE com JSON inválido, ou Decision Engine indisponível para segmento DMNVer "Observabilidade" abaixo

Observabilidade.

  • GET /feature-flags/health responde com nome e versão do build. É a sonda de vida do orquestrador. Não verifica banco nem Redis — um 200 aqui não garante que a avaliação funciona.
  • FlagEvaluationLog guarda 10% das avaliações com variationKey, reason, context, matchedRuleId, matchedSegmentIds e evaluationTimeMs. É a fonte para responder "qual a distribuição real do meu rollout de 10%?" e "quanto tempo custa avaliar esta flag?". Hoje só dá para consultar por SQL — não há endpoint (§15).
  • O evento feature-flags.flag.evaluated acompanha cada avaliação amostrada e pode ser consumido pelo Audit Trail ou entregue por webhook.
  • Chaves de cache para inspeção direta: ff:flag:{organizationId}:{flagKey} e ff:membership:{organizationId}:{segmentId}:{userId}.

14Segurança e compliance

Isolamento entre tenants. Os 25 endpoints aplicam, nesta ordem, authMiddlewarerequirePermission(...)requireOrganization. O organizationId sai do claim assinado do JWT e é passado explicitamente para cada método de repositório; nenhuma rota lê organizationId do corpo ou da query. As duas chaves únicas do banco são compostas com a organização ((organizationId, key) em FeatureFlag e em Segment), então duas empresas podem ter uma flag checkout-novo sem colidir e sem enxergar uma a outra. As chaves do Redis também carregam o organizationId no prefixo, o que impede que o cache vaze entre tenants.

Recursos de outra organização respondem 404, não 403 — deliberadamente, para não confirmar a existência do recurso.

Separação de privilégio para operação. FEATURE_FLAGS_TOGGLE é uma permissão distinta de FEATURE_FLAGS_UPDATE. Quem está de plantão pode receber só a de toggle: consegue desligar em incidente, não consegue alterar variação, criar flag nem mexer em segmento. É a diferença entre um kill switch operacional e uma porta aberta.

Dados sensíveis no contexto de avaliação. Este é o ponto de atenção do BB para LGPD. O context.attributes que você manda em cada avaliação é persistido inteiro no campo context (JSONB) de FlagEvaluationLog nas avaliações amostradas, e o context.userId é gravado em texto. O BB não filtra, não mascara e não classifica esse conteúdo.

A consequência prática: se o seu chamador mandar CPF, e-mail ou renda em attributes, esses valores ficam no banco. Recomendações:

  • Mande identificadores opacos, não dados pessoais. perfilRenda: "faixa-3" em vez de renda: 7200; um userId pseudonimizado em vez do CPF.
  • Mande só os atributos que as suas condições de fato usam. Atributo que não é lido por nenhuma condição só gera passivo.
  • Se dado pessoal precisar entrar no contexto, trate flags.flag_evaluation_logs como tabela com dado pessoal no seu inventário de LGPD, e considere que não há expurgo automático hoje (§15).

Segmentos DMN. O ruleContent de um segmento DMN é XML fornecido pelo cliente e executado pelo Decision Engine. A superfície de execução e as garantias de isolamento são as do Decision Engine, não deste BB. Trate quem tem SEGMENTS_CREATE como quem tem permissão de submeter lógica para execução no motor de decisão.

Exclusão lógica. Flags e segmentos usam deletedAt. A linha permanece para auditoria e para que um flagId histórico em log continue resolvível. Consultas de avaliação e listagem filtram deletedAt: null, então o recurso excluído é invisível pela API e sai do cache na mesma operação.

Rastreabilidade das mudanças. Toda criação, alteração, ligamento, desligamento e exclusão grava createdBy/updatedBy com o userId do token e publica evento no barramento com userId e organizationId nos metadados. "Quem desligou o checkout às 14h32" é uma consulta, não uma investigação.


15Limitações conhecidas

LimitaçãoImpactoSituação
evaluate-batch e evaluate-all não são alcançáveisOs handlers estão registrados como '-batch'/'-all' sem barra inicial; o Hono resolve para /evaluate/-batch e /evaluate/-all, e a rota POST /:flagKey, registrada antes, captura ambas e responde 404 Feature flag not found. Não existe hoje caminho HTTP para avaliação em lote.Bug conhecido — avalie uma flag por vez até a correção
inlineDmnRule não é avaliadoO campo existe no schema Prisma, é aceito na API e é devolvido nas consultas, mas o EvaluationService nunca o executa. Pior: uma regra com inlineDmnRule e sem segmentIds casa incondicionalmente, porque a checagem de segmentos encontra a lista vazia.Não implementado — use segmento do tipo DMN
Sem experimentação A/BO BB roteia de forma consistente e registra 10% das avaliações, mas não calcula significância, lift, intervalo de confiança nem sequer expõe os logs por API. Não é substituto de GrowthBook nem de Optimizely.Por design — ver §5
Sem API para os logs de avaliaçãoFlagEvaluationLog é escrito, e o repositório tem findMany, getStats e deleteOlderThan implementados, mas nenhum endpoint e nenhum job os chama. Os dados só saem por SQL.Implementado no repositório, não exposto
Sem retenção nos logs de avaliaçãoA tabela flags.flag_evaluation_logs cresce indefinidamente. Não há job de expurgo, e a configuração de retenção do Webhooks Engine não se aplica aqui.Roadmap — monitore o tamanho da tabela
Sem ambientes (dev/homologação/produção)Uma flag existe uma vez por organização. Não há promoção de configuração entre ambientes; a separação é por instância do BB.Por design — todos os cinco concorrentes de §5 têm
Sem SDK oficialIntegração é HTTP direto. Não há cliente que mantenha flags em memória, receba atualização por streaming nem faça fallback offline. Cada avaliação é uma chamada de rede (mitigada por cache do lado do servidor, não do cliente).Roadmap
ruleType de segmento é imutávelO schema de PATCH de segmento não aceita ruleType. Para mudar de SIMPLE para DMN, crie outro segmento e reaponte as regras.Por design
Segmento DMN sem ruleContent sempre casaDevolve true, liberando para todos — o oposto do que se espera de restrição. Segmento DMN incompleto é falha aberta.Comportamento conhecido — valide antes de apontar regra para ele
Falha do Decision Engine derruba a avaliaçãoSegmento DMN não tem fallback: se o motor está fora, a avaliação da flag responde 500 em vez de devolver a variação padrão.Por design — para caminho crítico, use segmento SIMPLE
links.self inconsistente e, em parte, incorretoFlags e segmentos devolvem /api/v1/flags/:id (sem o prefixo /feature-flags). Regras de targeting e overrides devolvem /api/v1/feature-flags/:flagId/..., um caminho que não existe.Bug conhecido — monte as URLs a partir de §9, não do links
Invalidação de cache de segmento usa KEYSinvalidateSegmentCache roda KEYS ff:membership:{org}:{seg}:*, comando que percorre todo o keyspace e bloqueia o Redis. Em base grande, alterar um segmento tem custo operacional proporcional ao tamanho total do Redis, não ao número de chaves afetadas.Gargalo conhecido
Override não expiraNão há validade nem limpeza automática. Overrides acumulam silenciosamente.Roadmap — revise periodicamente
Sem aprovação, sem agendamento, sem flag temporáriaLigar é imediato e não passa por fluxo de aprovação. Não há "ligar às 3h de terça" nem "expira em 30 dias".Não implementado
Sem limpeza de flag obsoletaO BB não detecta nem remove flag ligada em 100% há meses. A dívida de toggle continua sendo disciplina do time (§4, caso 4).Por design

16Perguntas frequentes

Quanto tempo leva entre eu desligar a flag e o efeito aparecer?

Imediato para quem consultar depois do POST /disable, porque a própria chamada invalida a chave ff:flag:{org}:{key} no Redis. O teto teórico é o TTL de 300 segundos, que só entra em jogo se a invalidação falhar ou se alguém mudar o status direto no banco em vez de usar a API.

O mesmo usuário sempre recebe a mesma variação?

Sim, enquanto a key da flag e o percentual não mudarem. O balde vem de murmurhash.v3("userId:flagKey") % 100, que é determinístico e não guarda estado. Aumentar o percentual só acrescenta usuários; diminuir remove. Trocar a key da flag reembaralha todo mundo — não faça isso durante um rollout.

Preciso mandar userId em toda avaliação?

Não é obrigatório, mas sem ele você perde duas coisas: overrides não são aplicados e qualquer regra com rolloutPercentage abaixo de 100 é pulada. Uma flag com rollout de 10% chamada sem userId devolve a variação padrão, sempre. Se a sua resposta parece "presa" no padrão, essa é a primeira coisa a conferir.

Dá para usar isso como plataforma de teste A/B?

Para o roteamento, sim: variações múltiplas, distribuição consistente e o registro de qual usuário viu o quê (amostrado em 10%). Para a análise, não — o BB não calcula significância, não conhece as suas métricas de negócio e nem sequer expõe os logs por API hoje (§15). Se a sua pergunta é "a variação B converte mais?", use GrowthBook ou o seu data warehouse. Se a pergunta é "quero liberar para 10% e poder desligar", é exatamente para isso que este BB existe.

Qual a diferença entre segmento SIMPLE e DMN?

SIMPLE é uma lista de condições sobre atributos, avaliada em memória, sem chamada de rede: rápido e sem dependência externa. DMN é uma tabela de decisão executada pelo Decision Engine: expressa regra de negócio de verdade, permite que o time de risco escreva o critério sem passar por deploy, e custa uma chamada de rede (cacheada por 15 minutos). Regra de ouro: se cabe em condições sobre atributos, use SIMPLE. Se a regra já existe como matriz de decisão do negócio, use DMN.

Uma flag da minha empresa pode ser vista por outro cliente da plataforma?

Não. O organizationId vem do JWT assinado e é aplicado em toda consulta ao banco e em todo prefixo de cache. A unicidade da key é por organização, então nem a colisão de nome existe. Recurso de outro tenant responde 404, não 403 — de propósito, para não confirmar que ele existe.

Posso avaliar várias flags de uma vez para carregar a tela?

Hoje não pela API. Os endpoints evaluate-batch e evaluate-all existem no código, mas estão registrados num caminho que a rota POST /:flagKey captura antes, então respondem 404 (§15). Até a correção, faça uma chamada por flag no bootstrap — o cache de 300 segundos do lado do servidor absorve boa parte do custo.

O que acontece com o dado que eu mando em context.attributes?

Ele é usado na avaliação e, em 10% das vezes, persistido inteiro em flags.flag_evaluation_logs, junto com o userId. Não há mascaramento nem expurgo automático. Mande identificador opaco e faixa em vez de dado pessoal bruto — ver §14.

Como faço para saber quais flags posso apagar?

GET /feature-flags/api/v1/flags?filter[status]=ENABLED devolve as ligadas; cruze com o SELECT em flag_evaluation_logs para ver quais ainda são consultadas. O BB não faz essa limpeza sozinho — flag ligada em 100% há seis meses é dívida técnica, e o material de referência do assunto trata as flags no código como estoque com custo de manutenção (Martin Fowler — *Feature Toggles*).

Preciso do Decision Engine para usar o Feature Flags?

Só se você usar segmento do tipo DMN. Flags com rollout percentual, overrides e segmentos SIMPLE funcionam sem nenhuma dependência além de PostgreSQL, Redis e IAM.


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md

Building blocks relacionados