Feature Flags
ProduçãoLigue e desligue funcionalidade em produção sem novo deploy, por segmento de cliente
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
26 endpoints em 6 recursos.
/feature-flags/api/v1/flags/feature-flags/api/v1/segments/feature-flags/api/v1/flags/feature-flags/api/v1/flags/feature-flags/api/v1/evaluate/feature-flags/healthResumo 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) |
O problema
negócioO 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?".
Sem um interruptor, só existe um caminho — e ele leva a funcionalidade para todo mundo ao mesmo tempo:
flowchart LR
A["código pronto"] --> B["deploy"]
B --> C["ligado para 100%<br/>dos clientes"]
C --> D{"quebrou?"}
D -->|não| E["ok"]
D -->|sim| F["rollback de deploy:<br/>pipeline + aprovação + janela"]
F --> BO 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 um princípio de entrega contínua.
Atenção. O princípio é "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.
Proposta de valor
negó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.
| Quem tem | Consegue | Não consegue |
|---|---|---|
Só FEATURE_FLAGS_TOGGLE | Ligar e desligar flag | Criar flag, mudar variação, mexer em segmento |
Só FEATURE_FLAGS_UPDATE | Alterar definição, regras e overrides | Ligar ou desligar a flag |
Só FEATURE_FLAGS_EVALUATE | Avaliar flag para um contexto | Enxergar ou alterar a configuração |
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.
flowchart LR
A["userId + flagKey"] --> B["murmurhash v3"]
B --> C["balde de 0 a 99"]
C --> D{"balde < rolloutPercentage?"}
D -->|sim| E["recebe a variação da regra"]
D -->|não| F["segue para a próxima regra"]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.
Casos de uso reais
negócioCaso 1 — Uma financeira muda a política de crédito para 3 correspondentes antes de mudar para 120 Cenário ilustrativo
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.
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 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.
sequenceDiagram
participant E as Esteira de crédito
participant FF as Feature Flags
participant P as Política de aceite
E->>FF: "POST /evaluate/politica-aceite-v2"
Note over E,FF: "context.attributes: correspondenteId, perfil"
FF->>FF: "segmento SIMPLE: correspondenteId in [corr-014, corr-022, corr-031]"
alt "correspondente no piloto"
FF-->>E: "variationKey v2, reason RULE_MATCH"
E->>P: "aplica o corte de score novo"
else "demais 117 correspondentes"
FF-->>E: "variationKey v1, reason DEFAULT"
E->>P: "aplica o corte de score atual"
endA 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
Plataforma de pagamentos que integra com um provedor externo de antifraude. O provedor tem incidente de latência algumas vezes por ano.
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 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.
flowchart LR
T["transação chega"] --> AV["avalia antifraude-externo-ativo"]
AV --> Q{"valor da variação"}
Q -->|true| PROV["chama o provedor externo<br/>timeout de 8s"]
Q -->|false| SEG["segue o fluxo sem o provedor"]
PL["plantão com FEATURE_FLAGS_TOGGLE"] -->|"POST /:flagId/disable"| AV
AV -->|"evento feature-flags.flag.disabled<br/>com o userId de quem desligou"| AUD["Audit Trail"]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
Plataforma B2B de seguros que vende módulos separados. Nem todo cliente contrata o módulo de sinistro digital.
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.
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.
flowchart LR SEG["segmento<br/>clientes-com-sinistro-digital"] --> FLAG["flag<br/>modulo-sinistro-digital"] FLAG --> FE["frontend<br/>avalia no bootstrap<br/>e mostra ou esconde o menu"] FLAG --> BE["backend<br/>avalia antes de expor a rota"] COM["comercial fecha contrato novo"] -->|"PATCH no segmento"| SEG
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
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 aqui é do mercado inteiro, e 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 a taxonomia: 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.
| Categoria da taxonomia | Para quê serve | No Feature Flags |
|---|---|---|
| Release toggle | Código incompleto em produção, desligado | ✅ POST /:flagId/enable e /disable |
| Ops toggle | Degradar funcionalidade durante incidente | ✅ Permissão FEATURE_FLAGS_TOGGLE separada |
| Permissioning toggle | Liberar por tipo de usuário ou cliente | ✅ Segmento SIMPLE ou DMN |
| Experiment toggle | A/B com análise estatística | ❌ Roteia e registra, mas não calcula significância (§15) |
Uma implementação honesta de três das quatro categorias, com a quarta claramente marcada como fora de escopo em vez de insinuada.
Mercado e diferenciais
negócioPanorama
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
Três situações em que a resposta honesta é "não use o nosso":
| Se o que você precisa é | Escolha | Porque nós |
|---|---|---|
| Experimentação A/B de verdade | GrowthBook | Não calculamos significância (§15) |
| Avaliação sem chamada de rede, com SDK em memória | LaunchDarkly ou Unleash | Exigimos uma chamada HTTP por avaliação |
| Ambientes separados com promoção de flag entre eles | Qualquer um dos cinco | Não temos ambientes |
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.
Modelo de cobrança e ROI
negócioUnidade 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.
flowchart TD
EV["avaliação chega"] --> C{"flag no cache Redis?"}
C -->|"sim, 300s de TTL"| BARATO["custo: 1 leitura no Redis"]
C -->|não| DB["custo: 1 leitura no Redis + 1 consulta ao Postgres"]
BARATO --> S{"a regra usa segmento?"}
DB --> S
S -->|não| PCT["rollout percentual:<br/>só aritmética em memória"]
S -->|SIMPLE| MEM["condições avaliadas em memória"]
S -->|DMN| M{"associação no cache?"}
M -->|"sim, 900s de TTL"| MEM2["custo: 1 leitura no Redis"]
M -->|não| DE["custo: 1 chamada de rede<br/>ao Decision Engine"]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ê.
Arquitetura
Camadas e caminho da requisição
flowchart TD
HTTP["HTTP"] --> APP
subgraph APP["Hono app — basePath /feature-flags"]
R1["/api/v1/flags — flagsRouter (7)"]
R2["/api/v1/flags/:flagId/targeting-rules — targetingRulesRouter (6)"]
R3["/api/v1/flags/:flagId/overrides — overridesRouter (4)"]
R4["/api/v1/segments — segmentsRouter (5)"]
R5["/api/v1/evaluate — evaluateRouter (3)"]
R6["/health"]
end
APP -->|"authMiddleware → requirePermission → requireOrganization<br/>Zod parse → ResultAsync<T, AppError>"| SVC
subgraph SVC["services/"]
S1["FlagService — CRUD, enable/disable, valida variações"]
S2["SegmentService — CRUD de segmentos"]
S3["EvaluationService — precedência, cache, log amostrado"]
S4["SegmentMembershipService — SIMPLE em memória · DMN via facade"]
end
SVC --> REPO["repositories/<br/>Prisma<br/>schema flags"]
SVC --> REDIS["Redis<br/>ff:flag:{org}:{key} — TTL 300s<br/>ff:membership:{org}:{seg}:{user} — TTL 900s"]
SVC --> DEF["DecisionEngineFacade<br/>local = TypeDI<br/>remote = HTTP"]Precedência de avaliação
A ordem importa e é fixa. Ela vale para evaluateFlag, evaluateBatch e evaluateAll — as três passam pelo mesmo evaluateFlagInternal.
flowchart TD
IN["POST /feature-flags/api/v1/evaluate/:flagKey<br/>context: userId, attributes"] --> C0["busca a flag: cache ff:flag:{org}:{key}, senão Postgres"]
C0 --> Q1{"1 · a flag existe nesta organização?"}
Q1 -->|não| E404["404 NOT_FOUND"]
Q1 -->|sim| Q2{"2 · a defaultVariation existe na lista de variações?"}
Q2 -->|não| E500["500 INTERNAL — dado inconsistente"]
Q2 -->|sim| Q3{"3 · status igual a DISABLED?"}
Q3 -->|sim| RDIS["variação padrão · reason DISABLED"]
Q3 -->|não| Q4{"4 · há userId no contexto e override para ele?"}
Q4 -->|sim| ROVR["variação do override · reason OVERRIDE"]
Q4 -->|não| Q5{"5 · a flag tem alguma regra de targeting?"}
Q5 -->|não| RDEF1["variação padrão · reason DEFAULT"]
Q5 -->|sim| LOOP["6 · percorre as regras por priority ASC<br/>menor priority primeiro"]
LOOP --> Q6{"6a · todos os segmentos da regra casam?<br/>lógica AND · regra sem segmento casa com todos"}
Q6 -->|não| NEXT["próxima regra"]
Q6 -->|sim| Q7{"6b · rolloutPercentage cobre este usuário?<br/>sem userId, regra com rollout menor que 100 é pulada"}
Q7 -->|não| NEXT
Q7 -->|sim| RMATCH["variação da regra · reason RULE_MATCH<br/>com matchedRuleId e matchedSegmentIds"]
NEXT --> Q8{"acabaram as regras?"}
Q8 -->|não| LOOP
Q8 -->|sim| RDEF2["7 · nenhuma regra casou<br/>variação padrão · reason DEFAULT"]reason na resposta | Significa | Onde olhar primeiro |
|---|---|---|
DISABLED | A flag está desligada | POST /:flagId/enable |
OVERRIDE | Existe fixação para este userId | GET /:flagId/overrides |
RULE_MATCH | Uma regra casou; matchedRuleId diz qual | priority das regras |
DEFAULT | Nenhuma regra casou, ou a flag não tem regra | Atributos enviados e userId |
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.
| Cache | Chave | TTL | Muda com que frequência | Custo de recalcular |
|---|---|---|---|---|
| Flag | ff:flag:{org}:{key} | 300s | Alta — é o ponto da flag | Uma consulta ao Postgres |
| Associação a segmento | ff:membership:{org}:{seg}:{user} | 900s | Baixa | Aritmética em memória (SIMPLE) ou chamada de rede (DMN) |
Atenção. 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.
flowchart LR
U{"há userId no contexto?"}
U -->|sim| H["murmurhash.v3 de userId e flagKey<br/>módulo 100, dá um balde de 0 a 99"]
H --> C{"balde menor que rolloutPercentage?"}
C -->|sim| IN["a regra casa"]
C -->|não| OUT["a regra é pulada"]
U -->|não| SK["regra com rollout abaixo de 100% é pulada<br/>sortear daria resposta diferente a cada requisição"]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.
| Você quer | Como monta |
|---|---|
| Segmento A e segmento B | Uma regra com os dois segmentIds |
| Segmento A ou segmento B | Duas regras com a mesma variationKey |
| Condição X ou condição Y | Um segmento SIMPLE com operator: "OR" |
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.
flowchart LR SMS["SegmentMembershipService"] --> IF["IDecisionEngineFacade"] IF -->|"DEPLOYMENT_MODE=monolith"| LOCAL["chamada direta no container TypeDI"] IF -->|"DEPLOYMENT_MODE=standalone"| REMOTE["HTTP para MODULE_DECISION_ENGINE_URL"]
Nos dois modos o BB expõe apenas HTTP: não há consumidor de fila nem job agendado no main.ts.
Conceitos 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.
erDiagram
FeatureFlag ||--o{ FlagTargetingRule : "tem regras"
FeatureFlag ||--o{ FlagOverride : "tem fixações"
FeatureFlag ||--o{ FlagEvaluationLog : "gera avaliações amostradas"
FlagTargetingRule ||--o{ TargetingRuleSegment : "aponta para"
Segment ||--o{ TargetingRuleSegment : "é usado por"
FeatureFlag {
uuid id
uuid organizationId
string key "único por organização"
enum status "ENABLED ou DISABLED"
jsonb variations
string defaultVariation
datetime deletedAt "exclusão lógica"
}
Segment {
uuid id
uuid organizationId
string key "único por organização"
enum ruleType "SIMPLE ou DMN"
text ruleContent
string decisionId
datetime deletedAt "exclusão lógica"
}
FlagTargetingRule {
uuid id
uuid flagId
int priority "menor avalia primeiro"
string variationKey
int rolloutPercentage
text inlineDmnRule "aceito, não avaliado"
}
TargetingRuleSegment {
uuid ruleId "chave composta"
uuid segmentId "chave composta"
}
FlagOverride {
uuid id
uuid flagId
string userId "único junto com flagId"
string variationKey
}
FlagEvaluationLog {
uuid id
string flagKey
string variationKey
string reason
jsonb context
int evaluationTimeMs
}| 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
stateDiagram-v2
[*] --> DISABLED : "POST /flags — nasce aqui sempre"
DISABLED --> ENABLED : "POST /:flagId/enable"
ENABLED --> DISABLED : "POST /:flagId/disable"
DISABLED --> Excluida : "DELETE /:flagId"
ENABLED --> Excluida : "DELETE /:flagId"
Excluida --> [*]
state "deletedAt diferente de null" as Excluida
note right of Excluida
Exclusão lógica: a linha fica no banco,
a flag some das consultas e do cache.
end noteSó 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.
Referê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.
flowchart LR BASE["/feature-flags/api/v1"] --> F["/flags — 7 endpoints"] BASE --> S["/segments — 5 endpoints"] BASE --> EV["/evaluate — 3 endpoints"] F --> TR["/flags/:flagId/targeting-rules — 6 endpoints"] F --> OV["/flags/:flagId/overrides — 4 endpoints"] TR -.->|"segmentIds"| S EV -.->|"lê flag, regras e overrides"| F
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"
}{
"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"
}
}
}{
"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"
}
}
}{
"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"]
}
}{
"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"]
}{
"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" }{ "userId": "cli-88213", "variationKey": "v2" }Override vence tudo, inclusive rollout percentual. Um userId só pode ter um override por flag — o segundo devolve 409.
Início rápido
Do zero à primeira flag ligada para 10% dos usuários. Credenciais de staging conforme AMBIENTES.md.
Atenção. Os comandos abaixo não foram executados na geração deste documento. Confira as respostas contra o seu ambiente.
O caminho tem sete passos e termina com a flag desligada de novo — de propósito, para você ver os dois lados do interruptor:
flowchart LR P1["1 · autenticar"] --> P2["2 · criar a flag"] P2 --> P3["3 · avaliar: nasce DISABLED"] P3 --> P4["4 · criar a regra de 10%"] P4 --> P5["5 · ligar a flag"] P5 --> P6["6 · ver o rollout acontecendo"] P6 --> P7["7 · desligar"]
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/v1TOKEN=$(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/v1Resposta esperada: $TOKEN preenchido com um JWT que carrega organizationId e as permissões FEATURE_FLAGS_*. Se vier vazio, a autenticação falhou e todos os passos seguintes respondem 401.
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"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"8f2c1f6e-1c2b-4a0f-9d1a-3b7c5e9a00118f2c1f6e-1c2b-4a0f-9d1a-3b7c5e9a00113. 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"}}' | jqcurl -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" } }{ "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}' | jqcurl -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{
"data": {
"type": "targeting-rules",
"id": "b1c2d3e4-0000-4000-8000-000000000001",
"attributes": {
"name": "Rollout 10%",
"priority": 0,
"variationKey": "on",
"rolloutPercentage": 10,
"segmentIds": []
}
}
}{
"data": {
"type": "targeting-rules",
"id": "b1c2d3e4-0000-4000-8000-000000000001",
"attributes": {
"name": "Rollout 10%",
"priority": 0,
"variationKey": "on",
"rolloutPercentage": 10,
"segmentIds": []
}
}
}Sem segmentIds, a regra casa com todo mundo e o filtro fica todo por conta do percentual.
5. Ligar a flag
curl -s -X POST $FF/flags/$FLAG_ID/enable \
-H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'curl -s -X POST $FF/flags/$FLAG_ID/enable \
-H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'"ENABLED""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 -cfor 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 27 off
3 on 27 off
3 onEspere 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'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.
Receitas
Subir um rollout de 5% para 100% com segurança
Objetivo. Aumentar a exposição em degraus sem trocar quem já está dentro.
Cada degrau só acrescenta usuários — quem já estava dentro continua dentro, porque o balde não muda:
flowchart LR A["5%"] --> B["10%"] B --> C["25%"] C --> D["50%"] D --> E["100%"] A -.->|"o conjunto só cresce"| E F["reduzir o percentual"] -.->|"remove quem já viu a novidade"| G["prefira desligar a flag"]
1. Descobrir o id da regra
RULE_ID=$(curl -s $FF/flags/$FLAG_ID/targeting-rules \
-H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')
echo "$RULE_ID"RULE_ID=$(curl -s $FF/flags/$FLAG_ID/targeting-rules \
-H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')
echo "$RULE_ID"b1c2d3e4-0000-4000-8000-000000000001b1c2d3e4-0000-4000-8000-0000000000012. Subir de degrau em degrau, observando métricas entre eles
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
donefor 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
done5
10
25
50
1005
10
25
50
100Armadilhas.
- 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.
São três peças, montadas nesta ordem:
flowchart LR RC["ruleContent<br/>string com JSON dentro"] --> SEG["segmento SIMPLE<br/>sp-renda-alta"] SEG --> RULE["regra de targeting<br/>segmentIds: [sp-renda-alta]"] RULE --> FLAG["flag"] CTX["context.attributes<br/>uf, renda"] -.->|"comparados na avaliação"| RC
1. Montar o ruleContent
# O ruleContent é uma STRING com JSON dentro. Monte com jq para não errar o escape.
REGRA=$(jq -nc '{
operator: "AND",
conditions: [
{ attribute: "uf", operator: "eq", value: "SP" },
{ attribute: "renda", operator: "gt", value: 5000 }
]
}')
echo "$REGRA"# O ruleContent é uma STRING com JSON dentro. Monte com jq para não errar o escape.
REGRA=$(jq -nc '{
operator: "AND",
conditions: [
{ attribute: "uf", operator: "eq", value: "SP" },
{ attribute: "renda", operator: "gt", value: 5000 }
]
}')
echo "$REGRA"{"operator":"AND","conditions":[{"attribute":"uf","operator":"eq","value":"SP"},{"attribute":"renda","operator":"gt","value":5000}]}{"operator":"AND","conditions":[{"attribute":"uf","operator":"eq","value":"SP"},{"attribute":"renda","operator":"gt","value":5000}]}2. Criar o segmento
SEG_ID=$(jq -nc --arg rc "$REGRA" '{
key: "sp-renda-alta",
name: "SP com renda acima de 5k",
ruleType: "SIMPLE",
ruleContent: $rc
}' \
| curl -s -X POST $FF/segments \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @- | jq -r '.data.id')
echo "$SEG_ID"SEG_ID=$(jq -nc --arg rc "$REGRA" '{
key: "sp-renda-alta",
name: "SP com renda acima de 5k",
ruleType: "SIMPLE",
ruleContent: $rc
}' \
| curl -s -X POST $FF/segments \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @- | jq -r '.data.id')
echo "$SEG_ID"a9f0e1d2-0000-4000-8000-000000000002a9f0e1d2-0000-4000-8000-0000000000023. Apontar uma regra da flag para o segmento
jq -nc --arg seg "$SEG_ID" '{
name: "SP renda alta", priority: 0,
variationKey: "on", segmentIds: [$seg]
}' \
| curl -s -X POST $FF/flags/$FLAG_ID/targeting-rules \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @-jq -nc --arg seg "$SEG_ID" '{
name: "SP renda alta", priority: 0,
variationKey: "on", segmentIds: [$seg]
}' \
| curl -s -X POST $FF/flags/$FLAG_ID/targeting-rules \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @-A resposta é a mesma da criação de regra em §9: 201 com data.attributes.segmentIds já preenchido com o 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.
sequenceDiagram
participant App as Aplicação
participant FF as Feature Flags
participant R as Redis
participant DE as Decision Engine
App->>FF: "POST /evaluate/:flagKey com context"
FF->>R: "GET ff:membership:{org}:{seg}:{user}"
alt "associação em cache"
R-->>FF: "true ou false"
else "cache frio"
FF->>DE: "execute(dmnXml, { userId, ...attributes })"
DE-->>FF: "{ isMember: true }"
FF->>R: "SETEX por 900s"
end
FF-->>App: "variação e reason"1. Criar o segmento DMN
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>"
}'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>"
}'2. Nomear a saída da tabela para que o BB a entenda
O SegmentMembershipService manda { userId, ...attributes } como entrada da decisão e interpreta a saída nesta ordem:
| Ordem | O que a decisão devolveu | Como vira pertinência |
|---|---|---|
| 1 | Um booleano | Usado direto |
| 2 | Um objeto com isMember | Boolean(isMember) |
| 3 | Um objeto com match | Boolean(match) |
| 4 | Um objeto com result | Boolean(result) |
| 5 | Qualquer outra coisa | 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.
O campo reason da resposta já diz por onde começar — ele é o passo da precedência em que a avaliação parou:
flowchart TD
R{"reason da resposta"}
R -->|DISABLED| D["a flag está desligada<br/>POST /:flagId/enable"]
R -->|OVERRIDE| O["existe fixação para o userId<br/>apague o override"]
R -->|DEFAULT| DF["nenhuma regra casou<br/>confira os atributos enviados<br/>contra as condições do segmento"]
R -->|"RULE_MATCH com a variação errada"| RM["confira priority: menor número ganha<br/>e a primeira que casa encerra a avaliação"]
DF --> U{"você mandou context.userId?"}
U -->|não| SEMID["regra com rollout menor que 100 é pulada"]
U -->|sim| ATR["compare atributo por atributo, com atenção ao tipo"]1. Ver 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}}}' | jqcurl -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{ "data": { "flagKey": "checkout-novo", "value": false,
"variationKey": "off", "reason": "DEFAULT" } }{ "data": { "flagKey": "checkout-novo", "value": false,
"variationKey": "off", "reason": "DEFAULT" } }2. Conferir se a flag está ligada
curl -s $FF/flags/$FLAG_ID -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'curl -s $FF/flags/$FLAG_ID -H "Authorization: Bearer $TOKEN" | jq '.data.attributes.status'"ENABLED""ENABLED"3. Conferir se existe override para este usuário
curl -s "$FF/flags/$FLAG_ID/overrides" -H "Authorization: Bearer $TOKEN" \
| jq '.data[] | select(.attributes.userId=="cli-88213")'curl -s "$FF/flags/$FLAG_ID/overrides" -H "Authorization: Bearer $TOKEN" \
| jq '.data[] | select(.attributes.userId=="cli-88213")'Saída vazia significa que não há override — se reason veio OVERRIDE, é aqui que a explicação aparece.
4. Conferir 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}'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}'{ "priority": 0, "variationKey": "on", "rollout": 10, "segs": [] }{ "priority": 0, "variationKey": "on", "rollout": 10, "segs": [] }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.
Atençã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
Objetivo. Tirar um usuário específico da funcionalidade nova sem mexer no rollout de todos os outros.
curl -s -X POST $FF/flags/$FLAG_ID/overrides \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"userId":"cli-88213","variationKey":"off"}'curl -s -X POST $FF/flags/$FLAG_ID/overrides \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"userId":"cli-88213","variationKey":"off"}'A partir da próxima avaliação, esse userId recebe reason: "OVERRIDE" — o override vence regra, segmento e rollout.
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.
Integraçã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 |
flowchart TD
IAM["IAM"] -->|"Bearer JWT — organizationId + permissões"| FF["Feature Flags<br/>avaliação · precedência · cache"]
FF -->|"segmento ruleType=DMN<br/>execute(dmnXml, { userId, ...attrs })"| DE["Decision Engine"]
DE -->|"{ isMember: true }"| FF
FF -->|"eventos feature-flags.*"| WH["Webhooks Engine<br/>entrega ao cliente final"]
FF -->|"eventos feature-flags.*"| AT["Audit Trail<br/>trilha de compliance"]
WH -->|"HTTP assinado"| CLI["sistema do cliente"]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.
Configuraçã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}.
Seguranç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.
flowchart LR REQ["requisição com Bearer JWT"] --> A["authMiddleware<br/>valida assinatura HS256"] A --> P["requirePermission<br/>FEATURE_FLAGS_* ou SEGMENTS_*"] P --> O["requireOrganization<br/>403 se o token não tem organizationId"] O --> ORG["organizationId sai do claim assinado"] ORG --> DB["toda consulta ao Postgres recebe o organizationId"] ORG --> RD["toda chave de Redis leva o organizationId no prefixo"]
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ão403— 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.
flowchart LR
CTX["context.attributes<br/>e context.userId"] --> AV["avaliação"]
AV --> S{"sorteio de 10%"}
S -->|"90% das vezes"| NADA["nada é gravado"]
S -->|"10% das vezes"| LOG["flags.flag_evaluation_logs<br/>context inteiro em JSONB<br/>userId em texto"]
LOG --> SEM["sem mascaramento<br/>e sem expurgo automático"]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.
Limitaçõ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 |
Perguntas 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