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.
- 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
- 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
- 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.
| Atributo | Valor |
|---|---|
| Identificador | feature-flags |
| Categoria | Plataforma |
| Escopo | Tenant (exige organizationId no token) |
| Porta (standalone) | 3010 |
| Path alias | @feature-flags |
| Prefixo HTTP | /feature-flags |
| Schema no banco | flags |
| Status | Produção desde 2025-11 |
| Depende de | PostgreSQL, 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
ifno código. Alguém escreveif (organizationId === '...'), o próximo escreve em outro lugar, e seis meses depois ninguém sabe quais clientes têm o quê. Descobrir exigegrep. - 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
| Antes | Depois |
|---|---|
| Subir código = ligar funcionalidade para todos | Sobe desligado; ligar é POST /:flagId/enable |
| Rollback é reverter deploy e esperar o pipeline | Rollback é desligar a flag, com efeito em até 5 minutos |
Liberação por cliente é if espalhado no código | Segmento nomeado, versionado e consultável por API |
| "Vamos testar com poucos" não tem como ser feito | Rollout percentual com bucketing consistente por usuário |
| Regra de segmentação complexa vira código | Tabela 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ério | Catalisa Feature Flags | LaunchDarkly | GrowthBook | Flagsmith | Unleash |
|---|---|---|---|---|---|
| Dimensão de cobrança | Avaliação e flag ativa | Conexão de serviço + MAU | Assento | Requisição de API | Assento |
| Preço público de entrada | Em definição | US$ 0 (Developer) | US$ 0 (self-host) | US$ 0 (50k req/mês) | US$ 75/assento/mês |
| Variação multivalorada (JSON) | Sim | Sim | Sim | Sim | Sim |
| Rollout percentual consistente | Sim (murmurhash v3) | Sim | Sim | Sim | Sim |
| Segmentação por atributo | Sim (11 operadores) | Sim | Sim | Sim | Sim |
| Regra de segmentação em DMN | Sim | Não | Não | Não | Não |
| Experimentação A/B com estatística | Não (ver §15) | Sim | Sim, forte | Parcial | Parcial |
| SDKs oficiais | Não — API HTTP (ver §15) | Sim, extenso | Sim | Sim | Sim |
| Ambientes (dev/homolog/prod) | Não (ver §15) | Sim | Sim | Sim | Sim |
| Avaliação sem chamada de rede | Não | Sim (streaming) | Sim | Sim | Sim |
| Multi-tenant com isolamento por token | Sim, do IAM | Via projeto/ambiente | Via organização | Via projeto | Via projeto |
| Self-host | Sim (é seu deploy) | Não | Sim | Sim | Sim |
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
- A segmentação pode ser uma tabela de decisão. Um
Segmentdo tipoDMNcarrega 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. - O tenant não é convenção. O
organizationIdvem do JWT assinado pelo IAM, e todos os 25 endpoints aplicamrequireOrganization. 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. - 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:
| Driver | Por que importa |
|---|---|
| Volume de avaliações de flag | É a chamada quente; cada uma consulta cache ou banco |
| Número de flags ativas | Estoque de configuração mantida e cacheada |
| Número de segmentos com regra DMN | Cada 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.
| Catalisa | LaunchDarkly (Foundation) | Unleash (PAYG) | GrowthBook (Pro) | |
|---|---|---|---|---|
| Base de cálculo | Avaliações + flags ativas | US$ 10 por Service Connection + US$ 8,33 por 1k MAU client-side | US$ 75 por assento, mínimo 5 | US$ 40 por assento |
| Conta do cenário | Em definição | 6 conexões + 40 mil MAU | 12 assentos | 12 assentos |
| Ordem de grandeza mensal | — | Centenas de dólares | ~US$ 900 | ~US$ 480 |
| Sobe se o cliente crescer? | Só com o volume de avaliações | Sim, com MAU | Não | Não |
| Sobe se o time crescer? | Não | Não | Sim | Sim |
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
ifque 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.createFlagforçastatus: 'DISABLED'mesmo que o corpo peça outra coisa. Criar não pode ser o mesmo ato de ligar — senão o primeiroPOSTde 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. OflagKeyentra 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
userIdno 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-batcheevaluate-allnã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. ParaOR, crie duas regras com a mesma variação, ou useoperator: "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
| Termo | Significa |
|---|---|
| Flag | A 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 variation | A 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. |
| Override | Fixação manual de uma variação para um userId específico. Vence qualquer regra. |
| Rollout percentage | Percentual de usuários da regra que recebem a variação. Bucketing consistente por userId:flagKey. |
| Evaluation context | O que você manda na avaliação: userId (opcional) e attributes (mapa de string/número/booleano/null). |
| Evaluation reason | Por que a resposta foi essa: DISABLED, OVERRIDE, RULE_MATCH ou DEFAULT. |
Modelo de dados — schema flags no PostgreSQL.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
FeatureFlag | flags.feature_flags | A flag | Único (organizationId, key), status, variations (JSONB), defaultVariation, deletedAt |
Segment | flags.segments | Conjunto de usuários | Único (organizationId, key), ruleType, ruleContent, decisionId, deletedAt |
FlagTargetingRule | flags.flag_targeting_rules | Regra de uma flag | flagId, priority, variationKey, rolloutPercentage, inlineDmnRule |
TargetingRuleSegment | flags.targeting_rule_segments | Ligação regra ↔ segmento | Chave composta (ruleId, segmentId) |
FlagOverride | flags.flag_overrides | Fixação por usuário | Único (flagId, userId), variationKey |
FlagEvaluationLog | flags.flag_evaluation_logs | Avaliações amostradas | flagKey, variationKey, reason, context, evaluationTimeMs |
Enumerações
| Enum | Valores |
|---|---|
FeatureFlagStatus | ENABLED · DISABLED |
SegmentRuleType | SIMPLE · 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 }:
| Operador | Aplica a | Observação |
|---|---|---|
eq · neq | Qualquer tipo | Comparação estrita (===) |
gt · gte · lt · lte | Números | Devolve false se qualquer lado não for número |
in · notIn | Lista | value precisa ser array |
contains · startsWith · endsWith | Strings | Devolve 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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /feature-flags/api/v1/flags | Cria flag (nasce DISABLED) | FEATURE_FLAGS_CREATE |
GET | /feature-flags/api/v1/flags | Lista flags, paginado, filtro filter[status] | FEATURE_FLAGS_READ |
GET | /feature-flags/api/v1/flags/:flagId | Busca flag por UUID | FEATURE_FLAGS_READ |
PATCH | /feature-flags/api/v1/flags/:flagId | Atualiza nome, descrição, variações e padrão | FEATURE_FLAGS_UPDATE |
POST | /feature-flags/api/v1/flags/:flagId/enable | Liga a flag | FEATURE_FLAGS_TOGGLE |
POST | /feature-flags/api/v1/flags/:flagId/disable | Desliga a flag | FEATURE_FLAGS_TOGGLE |
DELETE | /feature-flags/api/v1/flags/:flagId | Exclusão lógica. Responde 204 | FEATURE_FLAGS_DELETE |
Segmentos — /feature-flags/api/v1/segments
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /feature-flags/api/v1/segments | Cria segmento | SEGMENTS_CREATE |
GET | /feature-flags/api/v1/segments | Lista segmentos, paginado | SEGMENTS_READ |
GET | /feature-flags/api/v1/segments/:segmentId | Busca segmento por UUID | SEGMENTS_READ |
PATCH | /feature-flags/api/v1/segments/:segmentId | Atualiza nome, descrição, ruleContent, decisionId | SEGMENTS_UPDATE |
DELETE | /feature-flags/api/v1/segments/:segmentId | Exclusão lógica. Responde 204 | SEGMENTS_DELETE |
Regras de targeting — /feature-flags/api/v1/flags/:flagId/targeting-rules
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /feature-flags/api/v1/flags/:flagId/targeting-rules | Cria regra | FEATURE_FLAGS_UPDATE |
GET | /feature-flags/api/v1/flags/:flagId/targeting-rules | Lista regras da flag, por priority ASC | FEATURE_FLAGS_READ |
GET | /feature-flags/api/v1/flags/:flagId/targeting-rules/:ruleId | Busca uma regra | FEATURE_FLAGS_READ |
PATCH | /feature-flags/api/v1/flags/:flagId/targeting-rules/:ruleId | Atualiza regra | FEATURE_FLAGS_UPDATE |
DELETE | /feature-flags/api/v1/flags/:flagId/targeting-rules/:ruleId | Remove regra. Responde 204 | FEATURE_FLAGS_UPDATE |
POST | /feature-flags/api/v1/flags/:flagId/targeting-rules/reorder | Reordena por lista de IDs | FEATURE_FLAGS_UPDATE |
Overrides — /feature-flags/api/v1/flags/:flagId/overrides
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /feature-flags/api/v1/flags/:flagId/overrides | Fixa variação para um userId | FEATURE_FLAGS_UPDATE |
GET | /feature-flags/api/v1/flags/:flagId/overrides | Lista overrides, paginado | FEATURE_FLAGS_READ |
GET | /feature-flags/api/v1/flags/:flagId/overrides/:overrideId | Busca um override | FEATURE_FLAGS_READ |
DELETE | /feature-flags/api/v1/flags/:flagId/overrides/:overrideId | Remove override. Responde 204 | FEATURE_FLAGS_UPDATE |
Avaliação — /feature-flags/api/v1/evaluate
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /feature-flags/api/v1/evaluate/:flagKey | Avalia uma flag pela key | FEATURE_FLAGS_EVALUATE |
POST | /feature-flags/api/v1/evaluate/-batch | Avalia até 50 flags de uma vez | FEATURE_FLAGS_EVALUATE |
POST | /feature-flags/api/v1/evaluate/-all | Avalia todas as flags ENABLED (bootstrap) | FEATURE_FLAGS_EVALUATE |
⚠️
-batche-allestã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/-batche/-all. ComoPOST /:flagKeyé registrada antes, ela captura essas duas rotas primeiro e trata-batchcomo se fosse akeyde uma flag — o resultado é404 Feature flag not found. UsePOST /feature-flags/api/v1/evaluate/:flagKeyuma vez por flag até isso ser corrigido. Ver §15.
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /feature-flags/health | Sonda 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"
}
| Campo | Tipo | Obrigatório | Regra |
|---|---|---|---|
key | string | Sim | 1 a 100 caracteres, apenas [a-zA-Z0-9_-]. Única por organização |
name | string | Sim | 1 a 200 caracteres |
description | string | Não | Até 1000 caracteres |
variations | array | Sim | Ao menos 1. Cada item { key (1–50), value }; value aceita booleano, string, número, objeto ou lista |
defaultVariation | string | Sim | Precisa 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
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod, ou defaultVariation que não existe em variations |
401 | Token ausente ou inválido |
403 | Sem FEATURE_FLAGS_CREATE, ou token sem organizationId |
409 | key 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"
}
}
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
context.userId | string | Não | Identificador externo. Necessário para override e para rollout < 100% |
context.attributes | object | Não | Mapa 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
| Status | Quando |
|---|---|
400 | context fora do schema (por exemplo, atributo com objeto aninhado) |
403 | Sem FEATURE_FLAGS_EVALUATE, ou token sem organizationId |
404 | Flag inexistente, excluída logicamente, ou de outra organização |
500 | defaultVariation 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"]
}
| Campo | Tipo | Obrigatório | Regra |
|---|---|---|---|
name | string | Sim | 1 a 200 caracteres |
priority | int | Não | ≥ 0. Menor = avaliada primeiro. Omitido, o serviço usa a próxima disponível |
variationKey | string | Sim | Variação devolvida quando a regra casa |
rolloutPercentage | int | Não | 0 a 100, padrão 100. Abaixo de 100 exige context.userId |
segmentIds | uuid[] | Não | Combinados com AND. Lista vazia ou ausente = regra casa com todos |
inlineDmnRule | string | Não | Aceito 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
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod, ou flagId que não é UUID |
404 | Flag 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
keyda flag no meio do rollout. O balde éhash(userId:flagKey); trocar akeyreembaralha 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.userIdnunca 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 gera500na avaliação, não400na criação — a validação do conteúdo acontece na hora de avaliar.- Comparadores numéricos (
gt,gte,lt,lte) devolvemfalsequando qualquer lado não é número."renda": "7200"(string) nunca passa emgt: 5000. - O
ruleTypenão pode ser alterado depois: o schema dePATCHde segmento não aceita o campo. Para mudar deSIMPLEparaDMN, crie outro segmento. - Mudar
ruleContentinvalida 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
ruleContentsempre casa (devolvetrue). 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
500em vez de cair na variação padrão. Para caminho crítico, prefira segmentoSIMPLE.
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 block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token; fornece organizationId e as permissões FEATURE_FLAGS_* e SEGMENTS_* | Sim |
| Decision Engine | Executa segmentos do tipo DMN via IDecisionEngineFacade | Só para segmento DMN |
| Webhooks Engine | Entrega os eventos feature-flags.* para sistemas do cliente | Não |
| Audit Trail | Consome os mesmos eventos para trilha de compliance | Não |
| Decision Platform | Orquestra esteiras que consultam flags para escolher o caminho | Nã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:
| Evento | Quando |
|---|---|
feature-flags.flag.created | Flag criada |
feature-flags.flag.updated | Flag alterada |
feature-flags.flag.enabled | Flag ligada |
feature-flags.flag.disabled | Flag desligada |
feature-flags.flag.deleted | Flag excluída logicamente |
feature-flags.segment.created · .updated · .deleted | Ciclo de vida do segmento |
feature-flags.flag.evaluated | Avaliaçã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ável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
DATABASE_URL | PostgreSQL, schema flags | Sim | — |
REDIS_URL | Redis, usado nos dois caches | Sim | — |
JWT_SECRET | Segredo HS256 do IAM. Mínimo 44 caracteres | Sim | — |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
MODULE_DECISION_ENGINE_URL | Endereço do Decision Engine em standalone. Necessário para segmento DMN | Só em standalone com DMN | — |
PORT | Porta no modo standalone | Não | 3000 (mapeada para 3010 no compose) |
Dependências de infraestrutura
| Dependência | Para quê | Se cair |
|---|---|---|
| PostgreSQL | Schema flags | Avaliação falha com 500 |
| Redis | Cache de flag (300s) e de associação de segmento (900s) | Avaliação continua, direto no banco, com mais latência |
| IAM | Verificação do token (assinatura local, sem chamada de rede) | Nada muda enquanto o JWT_SECRET estiver correto |
| Decision Engine | Segmentos DMN | Avaliação da flag falha com 500 — não cai para a variação padrão |
Limites e quotas
| Limite | Valor | Onde |
|---|---|---|
Tamanho da key de flag ou segmento | 1 a 100 caracteres, [a-zA-Z0-9_-] | flagKeySchema |
Tamanho da key de variação | 1 a 50 caracteres | variationSchema |
| Variações por flag | Mínimo 1, sem máximo declarado | variationsSchema |
Flags por chamada em -batch | 1 a 50 | batchEvaluateInputSchema |
| Página máxima em listagens | 100 itens | listFlagsInputSchema, listSegmentsInputSchema |
rolloutPercentage | Inteiro de 0 a 100 | createTargetingRuleInputSchema |
userId de override | 1 a 255 caracteres | createOverrideInputSchema |
| TTL do cache de flag | 300 segundos | FLAG_CACHE_TTL |
| TTL do cache de associação | 900 segundos | MEMBERSHIP_CACHE_TTL |
| Amostragem do log de avaliação | 10% | EVALUATION_LOG_SAMPLE_RATE |
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado no Zod, defaultVariation inexistente na lista, ou parâmetro que não é UUID | Confira os tipos contra §9. :flagId é UUID; :flagKey é a key |
401 | UNAUTHORIZED | Token ausente, inválido ou expirado | Renove o token no IAM |
403 | FORBIDDEN | Permissão faltando ou token sem organizationId | Confira permissions no token; autentique informando a organização |
404 | NOT_FOUND | Flag, segmento, regra ou override inexistente, excluído, ou de outra organização | Confira o identificador. 404 também é a resposta para recurso de outro tenant — por design, para não permitir enumeração |
409 | CONFLICT | key de flag ou segmento repetida na organização, ou override já existente para o userId | Escolha outra key, ou apague o override antes |
500 | INTERNAL | defaultVariation inconsistente, ruleContent de segmento SIMPLE com JSON inválido, ou Decision Engine indisponível para segmento DMN | Ver "Observabilidade" abaixo |
Observabilidade.
GET /feature-flags/healthresponde com nome e versão do build. É a sonda de vida do orquestrador. Não verifica banco nem Redis — um200aqui não garante que a avaliação funciona.FlagEvaluationLogguarda 10% das avaliações comvariationKey,reason,context,matchedRuleId,matchedSegmentIdseevaluationTimeMs. É 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.evaluatedacompanha 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}eff:membership:{organizationId}:{segmentId}:{userId}.
14Segurança e compliance
Isolamento entre tenants. Os 25 endpoints aplicam, nesta ordem, authMiddleware → requirePermission(...) → 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 derenda: 7200; umuserIdpseudonimizado 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_logscomo 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ção | Impacto | Situação |
|---|---|---|
evaluate-batch e evaluate-all não são alcançáveis | Os 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 é avaliado | O 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/B | O 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ção | FlagEvaluationLog é 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ção | A 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 oficial | Integraçã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ável | O 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 casa | Devolve 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ção | Segmento 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, incorreto | Flags 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 KEYS | invalidateSegmentCache 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 expira | Não há validade nem limpeza automática. Overrides acumulam silenciosamente. | Roadmap — revise periodicamente |
| Sem aprovação, sem agendamento, sem flag temporária | Ligar é 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 obsoleta | O 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